jp: jp asyncio port:
/!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\ This patch implements the port of jp to asyncio, so it is now correctly using the bridge asynchronously, and it can be used with bridges like `pb`. This also simplify the code, notably for things which were previously implemented with many callbacks (like pagination with RSM). During the process, some behaviours have been modified/fixed, in jp and backends, check diff for details.
This commit is contained in:
parent
5cee4fbab7
commit
76aadbb3fb
60 changed files with 3299 additions and 3300 deletions
|
|
@ -334,6 +334,7 @@ class Const(object):
|
|||
|
||||
## Progress error special values ##
|
||||
PROGRESS_ERROR_DECLINED = "declined" # session has been declined by peer user
|
||||
PROGRESS_ERROR_FAILED = "failed" # something went wrong with the session
|
||||
|
||||
## Files ##
|
||||
FILE_TYPE_DIRECTORY = "directory"
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ class SAT(service.Service):
|
|||
)
|
||||
self._unimport_plugin(plugin_path)
|
||||
continue
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
log.error(
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ class SatXMPPEntity(object):
|
|||
self.actions = {} # used to keep track of actions for retrieval (key = action_id)
|
||||
self.encryption = encryption.EncryptionHandler(self)
|
||||
|
||||
def __unicode__(self):
|
||||
return "Client instance for profile {profile}".format(profile=self.profile)
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__.encode('utf-8')
|
||||
return f"Client for profile {self.profile}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"{super().__repr__()} - profile: {self.profile!r}"
|
||||
|
||||
## initialisation ##
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class BlockCipher(object):
|
|||
@param key (unicode): the encryption key
|
||||
@param text (unicode): the text to encrypt
|
||||
@param leave_empty (bool): if True, empty text will be returned "as is"
|
||||
@return: Deferred: base-64 encoded str
|
||||
@return (D(str)): base-64 encoded encrypted message
|
||||
"""
|
||||
if leave_empty and text == "":
|
||||
return succeed(text)
|
||||
|
|
@ -59,6 +59,7 @@ class BlockCipher(object):
|
|||
cipher = AES.new(key, AES.MODE_CFB, iv)
|
||||
d = deferToThread(cipher.encrypt, BlockCipher.pad(text.encode("utf-8")))
|
||||
d.addCallback(lambda ciphertext: b64encode(iv + ciphertext))
|
||||
d.addCallback(lambda bytes_cypher: bytes_cypher.decode('utf-8'))
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
|
|
@ -137,7 +138,7 @@ class PasswordHasher(object):
|
|||
@return: Deferred: base-64 encoded str
|
||||
"""
|
||||
if leave_empty and password == "":
|
||||
return succeed(b"")
|
||||
return succeed("")
|
||||
salt = (
|
||||
b64decode(salt)[: PasswordHasher.SALT_LEN]
|
||||
if salt
|
||||
|
|
@ -145,11 +146,12 @@ class PasswordHasher(object):
|
|||
)
|
||||
d = deferToThread(PBKDF2, password, salt)
|
||||
d.addCallback(lambda hashed: b64encode(salt + hashed))
|
||||
d.addCallback(lambda hashed_bytes: hashed_bytes.decode('utf-8'))
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def compare_hash(cls, hashed_attempt, hashed):
|
||||
assert isinstance(hashed, bytes)
|
||||
assert isinstance(hashed, str)
|
||||
return hashed_attempt == hashed
|
||||
|
||||
@classmethod
|
||||
|
|
@ -160,7 +162,9 @@ class PasswordHasher(object):
|
|||
@param hashed (str): the hash of the password
|
||||
@return: Deferred: boolean
|
||||
"""
|
||||
assert isinstance(attempt, str)
|
||||
assert isinstance(hashed, str)
|
||||
leave_empty = hashed == ""
|
||||
d = PasswordHasher.hash(attempt, hashed, leave_empty)
|
||||
d.addCallback(cls.compare_hash, hashed=hashed.encode('utf-8'))
|
||||
d.addCallback(cls.compare_hash, hashed=hashed)
|
||||
return d
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@ class Discovery(object):
|
|||
extensions[form_type or ""] = fields
|
||||
|
||||
defer.returnValue((
|
||||
disco_infos.features,
|
||||
[str(f) for f in disco_infos.features],
|
||||
[(cat, type_, name or "")
|
||||
for (cat, type_), name in list(disco_infos.identities.items())],
|
||||
extensions))
|
||||
|
|
|
|||
|
|
@ -560,9 +560,11 @@ class Memory(object):
|
|||
|
||||
def initPersonalKey(__):
|
||||
# be sure to call this after checking that the profile doesn't exist yet
|
||||
|
||||
# generated once for all and saved in a PersistentDict
|
||||
personal_key = BlockCipher.getRandomKey(
|
||||
base64=True
|
||||
) # generated once for all and saved in a PersistentDict
|
||||
).decode('utf-8')
|
||||
self.auth_sessions.newSession(
|
||||
{C.MEMORY_CRYPTO_KEY: personal_key}, profile=name
|
||||
) # will be encrypted by setParam
|
||||
|
|
@ -1260,8 +1262,8 @@ class Memory(object):
|
|||
parent = current["parent"]
|
||||
if not parent:
|
||||
break
|
||||
files_data = yield self.getFile(
|
||||
self, client, peer_jid=None, file_id=parent, perms_to_check=None
|
||||
files_data = yield self.getFiles(
|
||||
client, peer_jid=None, file_id=parent, perms_to_check=None
|
||||
)
|
||||
try:
|
||||
current = files_data[0]
|
||||
|
|
|
|||
|
|
@ -529,17 +529,18 @@ class Params(object):
|
|||
|
||||
return d.addCallback(gotPlainPassword)
|
||||
|
||||
def __type_to_string(self, result):
|
||||
""" convert result to string, according to its type """
|
||||
def _type_to_str(self, result):
|
||||
"""Convert result to string, according to its type """
|
||||
if isinstance(result, bool):
|
||||
return "true" if result else "false"
|
||||
elif isinstance(result, int):
|
||||
return C.boolConst(result)
|
||||
elif isinstance(result, (list, set, tuple)):
|
||||
return ', '.join(self._type_to_str(r) for r in result)
|
||||
else:
|
||||
return str(result)
|
||||
return result
|
||||
|
||||
def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
|
||||
""" Same as getParamA but for bridge: convert non string value to string """
|
||||
return self.__type_to_string(
|
||||
return self._type_to_str(
|
||||
self.getParamA(name, category, attr, profile_key=profile_key)
|
||||
)
|
||||
|
||||
|
|
@ -599,15 +600,10 @@ class Params(object):
|
|||
return self._getAttr(node[1], attr, value)
|
||||
|
||||
def asyncGetStringParamA(
|
||||
self,
|
||||
name,
|
||||
category,
|
||||
attr="value",
|
||||
security_limit=C.NO_SECURITY_LIMIT,
|
||||
profile_key=C.PROF_KEY_NONE,
|
||||
):
|
||||
self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT,
|
||||
profile_key=C.PROF_KEY_NONE):
|
||||
d = self.asyncGetParamA(name, category, attr, security_limit, profile_key)
|
||||
d.addCallback(self.__type_to_string)
|
||||
d.addCallback(self._type_to_str)
|
||||
return d
|
||||
|
||||
def asyncGetParamA(
|
||||
|
|
@ -957,14 +953,8 @@ class Params(object):
|
|||
categories.append(cat.getAttribute("name"))
|
||||
return categories
|
||||
|
||||
def setParam(
|
||||
self,
|
||||
name,
|
||||
value,
|
||||
category,
|
||||
security_limit=C.NO_SECURITY_LIMIT,
|
||||
profile_key=C.PROF_KEY_NONE,
|
||||
):
|
||||
def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT,
|
||||
profile_key=C.PROF_KEY_NONE):
|
||||
"""Set a parameter, return None if the parameter is not in param xml.
|
||||
|
||||
Parameter of type 'password' that are not the SàT profile password are
|
||||
|
|
@ -994,13 +984,11 @@ class Params(object):
|
|||
return defer.succeed(None)
|
||||
|
||||
if not self.checkSecurityLimit(node[1], security_limit):
|
||||
log.warning(
|
||||
_(
|
||||
"Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!"
|
||||
% {"param": name, "cat": category}
|
||||
)
|
||||
)
|
||||
return defer.succeed(None)
|
||||
msg = _(
|
||||
f"{profile!r} is trying to set parameter {name!r} in category "
|
||||
f"{category!r} without authorization!!!")
|
||||
log.warning(msg)
|
||||
raise exceptions.PermissionError(msg)
|
||||
|
||||
type_ = node[1].getAttribute("type")
|
||||
if type_ == "int":
|
||||
|
|
@ -1010,12 +998,10 @@ class Params(object):
|
|||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
log.debug(
|
||||
_(
|
||||
"Trying to set parameter '%(param)s' in category '%(cat)s' with an non-integer value"
|
||||
% {"param": name, "cat": category}
|
||||
)
|
||||
)
|
||||
log.warning(_(
|
||||
f"Trying to set parameter {name!r} in category {category!r} with"
|
||||
f"an non-integer value"
|
||||
))
|
||||
return defer.succeed(None)
|
||||
if node[1].hasAttribute("constraint"):
|
||||
constraint = node[1].getAttribute("constraint")
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class AdHocDBus(object):
|
|||
flags=flags,
|
||||
)
|
||||
|
||||
defer.returnValue((bus_name, methods))
|
||||
defer.returnValue((str(bus_name), methods))
|
||||
|
||||
def _addCommand(self, client, adhoc_name, bus_name, methods, allowed_jids=None,
|
||||
allowed_groups=None, allowed_magics=None, forbidden_jids=None,
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ class Events(object):
|
|||
|
||||
if register:
|
||||
yield self.register(
|
||||
client, service, None, {}, node, event_id, event_elt, creator=True)
|
||||
client, None, {}, service, node, event_id, item_elt, creator=True)
|
||||
defer.returnValue(node)
|
||||
|
||||
def _eventModify(self, service, node, id_, timestamp_update, data_update,
|
||||
|
|
@ -447,24 +447,29 @@ class Events(object):
|
|||
events.append((timestamp, data))
|
||||
defer.returnValue(events)
|
||||
|
||||
def _eventInviteeGet(self, service, node, profile_key):
|
||||
def _eventInviteeGet(self, service, node, invitee_jid_s, profile_key):
|
||||
service = jid.JID(service) if service else None
|
||||
node = node if node else NS_EVENT
|
||||
client = self.host.getClient(profile_key)
|
||||
return self.eventInviteeGet(client, service, node)
|
||||
invitee_jid = jid.JID(invitee_jid_s) if invitee_jid_s else None
|
||||
return self.eventInviteeGet(client, service, node, invitee_jid)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def eventInviteeGet(self, client, service, node):
|
||||
def eventInviteeGet(self, client, service, node, invitee_jid=None):
|
||||
"""Retrieve attendance from event node
|
||||
|
||||
@param service(unicode, None): PubSub service
|
||||
@param node(unicode): PubSub node of the event
|
||||
@param node(unicode): PubSub node of the event's invitees
|
||||
@param invitee_jid(jid.JId, None): jid of the invitee to retrieve (None to
|
||||
retrieve profile's invitation). The bare jid correspond to the PubSub item id.
|
||||
@return (dict): a dict with current attendance status,
|
||||
an empty dict is returned if nothing has been answered yed
|
||||
"""
|
||||
if invitee_jid is None:
|
||||
invitee_jid = client.jid
|
||||
try:
|
||||
items, metadata = yield self._p.getItems(
|
||||
client, service, node, item_ids=[client.jid.userhost()]
|
||||
client, service, node, item_ids=[invitee_jid.userhost()]
|
||||
)
|
||||
event_elt = next(items[0].elements(NS_EVENT, "invitee"))
|
||||
except (exceptions.NotFound, IndexError):
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import Iterable
|
||||
import copy
|
||||
import itertools
|
||||
from zope.interface import implementer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import defer
|
||||
from wokkel import disco, iwokkel
|
||||
from wokkel import data_form
|
||||
|
|
@ -174,7 +174,16 @@ class PubsubSchema(object):
|
|||
)
|
||||
elif isinstance(schema, data_form.Form):
|
||||
if copy_form:
|
||||
schema = copy.deepcopy(schema)
|
||||
# XXX: we don't use deepcopy as it will do an infinite loop if a
|
||||
# domish.Element is present in the form fields (happens for
|
||||
# XEP-0315 data forms XML Element)
|
||||
schema = data_form.Form(
|
||||
formType = schema.formType,
|
||||
title = schema.title,
|
||||
instructions = schema.instructions[:],
|
||||
formNamespace = schema.formNamespace,
|
||||
fields = schema.fieldList,
|
||||
)
|
||||
defer.returnValue(schema)
|
||||
|
||||
try:
|
||||
|
|
@ -394,6 +403,18 @@ class PubsubSchema(object):
|
|||
if not values_list:
|
||||
# if values don't map to allowed values, we use default ones
|
||||
values_list = field.values
|
||||
elif field.ext_type == 'xml':
|
||||
# FIXME: XML elements are not handled correctly, we need to know if we
|
||||
# actual XML/XHTML, or text to escape
|
||||
for idx, value in enumerate(values_list[:]):
|
||||
if not isinstance(value, domish.Element):
|
||||
if field.value and field.value.uri == C.NS_XHTML:
|
||||
div_elt = domish.Element((C.NS_XHTML, 'div'))
|
||||
div_elt.addContent(str(value))
|
||||
values_list[idx] = div_elt
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
field.values = values_list
|
||||
|
||||
yield self._p.sendItem(
|
||||
|
|
@ -546,9 +567,7 @@ class PubsubSchema(object):
|
|||
form = data_form.findForm(item_elt, form_ns)
|
||||
if form is None:
|
||||
log.warning(
|
||||
_(
|
||||
"Can't parse previous item, update ignored: data form not found"
|
||||
).format(reason=e)
|
||||
_("Can't parse previous item, update ignored: data form not found")
|
||||
)
|
||||
else:
|
||||
for name, field in form.fields.items():
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ImportPlugin(object):
|
|||
@param name(unicode): import handler name
|
||||
"""
|
||||
assert name == name.lower().strip()
|
||||
log.info(_("initializing {name} import handler").format(name=name))
|
||||
log.info(_(f"initializing {name} import handler"))
|
||||
import_handler.name = name
|
||||
import_handler.register = partial(self.register, import_handler)
|
||||
import_handler.unregister = partial(self.unregister, import_handler)
|
||||
|
|
@ -139,16 +139,8 @@ class ImportPlugin(object):
|
|||
else:
|
||||
return importer.short_desc, importer.long_desc
|
||||
|
||||
def _doImport(
|
||||
self,
|
||||
import_handler,
|
||||
name,
|
||||
location,
|
||||
options,
|
||||
pubsub_service="",
|
||||
pubsub_node="",
|
||||
profile=C.PROF_KEY_NONE,
|
||||
):
|
||||
def _doImport(self, import_handler, name, location, options, pubsub_service="",
|
||||
pubsub_node="", profile=C.PROF_KEY_NONE):
|
||||
client = self.host.getClient(profile)
|
||||
options = {key: str(value) for key, value in options.items()}
|
||||
for option in import_handler.BOOL_OPTIONS:
|
||||
|
|
@ -159,9 +151,11 @@ class ImportPlugin(object):
|
|||
for option in import_handler.JSON_OPTIONS:
|
||||
try:
|
||||
options[option] = json.loads(options[option])
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
raise exceptions.DataError(
|
||||
_("invalid json option: {name}").format(name=option)
|
||||
_(f"invalid json option: {option}")
|
||||
)
|
||||
pubsub_service = jid.JID(pubsub_service) if pubsub_service else None
|
||||
return self.doImport(
|
||||
|
|
@ -175,16 +169,8 @@ class ImportPlugin(object):
|
|||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def doImport(
|
||||
self,
|
||||
client,
|
||||
import_handler,
|
||||
name,
|
||||
location,
|
||||
options=None,
|
||||
pubsub_service=None,
|
||||
pubsub_node=None,
|
||||
):
|
||||
def doImport(self, client, import_handler, name, location, options=None,
|
||||
pubsub_service=None, pubsub_node=None,):
|
||||
"""Import data
|
||||
|
||||
@param import_handler(object): instance of the import handler
|
||||
|
|
@ -193,7 +179,8 @@ class ImportPlugin(object):
|
|||
can be an url, a file path, or anything which make sense
|
||||
check importer description for more details
|
||||
@param options(dict, None): extra options.
|
||||
@param pubsub_service(jid.JID, None): jid of the PubSub service where data must be imported
|
||||
@param pubsub_service(jid.JID, None): jid of the PubSub service where data must be
|
||||
imported.
|
||||
None to use profile's server
|
||||
@param pubsub_node(unicode, None): PubSub node to use
|
||||
None to use importer's default node
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class MercurialProtocol(async_process.CommandProtocol):
|
|||
|
||||
@param path(unicode): path to the repository
|
||||
@param command(unicode): hg command to run
|
||||
@return D(bytes): stdout of the command
|
||||
"""
|
||||
assert "path" not in kwargs
|
||||
kwargs["path"] = path
|
||||
|
|
@ -86,8 +87,10 @@ class MercurialHandler(object):
|
|||
return d
|
||||
|
||||
def export(self, repository):
|
||||
return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()',
|
||||
'--encoding=utf-8')
|
||||
d = MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()',
|
||||
'--encoding=utf-8')
|
||||
d.addCallback(lambda data: data.decode('utf-8'))
|
||||
return d
|
||||
|
||||
def import_(self, repository, data, data_type, item_id, service, node, extra):
|
||||
parsed_data = self.parse(data)
|
||||
|
|
@ -97,7 +100,7 @@ class MercurialHandler(object):
|
|||
except Exception:
|
||||
parsed_name = ''
|
||||
name = 'mr_{item_id}_{parsed_name}'.format(item_id=CLEAN_RE.sub('', item_id),
|
||||
parsed_name=parsed_name)
|
||||
parsed_name=parsed_name)
|
||||
return MercurialProtocol.run(repository, 'qimport', '-g', '--name', name,
|
||||
'--encoding=utf-8', '-', stdin=data)
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ class InvitationsPlugin(object):
|
|||
try:
|
||||
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
|
||||
password)
|
||||
except error.StanzaError as e:
|
||||
except error.StanzaError:
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
|
|
@ -321,7 +321,7 @@ class InvitationsPlugin(object):
|
|||
format_args['url'] = invite_url
|
||||
|
||||
yield sat_email.sendEmail(
|
||||
self.host,
|
||||
self.host.memory.config,
|
||||
[email] + emails_extra,
|
||||
(kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format(
|
||||
**format_args),
|
||||
|
|
@ -401,7 +401,7 @@ class InvitationsPlugin(object):
|
|||
C.PROF_KEY_NONE: don't filter invitations
|
||||
@return list(unicode): invitations uids
|
||||
"""
|
||||
invitations = yield list(self.invitations.items())
|
||||
invitations = yield self.invitations.items()
|
||||
if profile != C.PROF_KEY_NONE:
|
||||
invitations = {id_:data for id_, data in invitations.items()
|
||||
if data.get('profile') == profile}
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ class MergeRequests(object):
|
|||
@param name(unicode): name of the handler
|
||||
@param handler(object): instance of the handler.
|
||||
It must have the following methods, which may all return a Deferred:
|
||||
- check(repository): True if repository can be handled
|
||||
- export(repository): return export data, i.e. the patches
|
||||
- check(repository)->bool: True if repository can be handled
|
||||
- export(repository)->str: return export data, i.e. the patches
|
||||
- parse(export_data): parse report data and return a list of dict
|
||||
(1 per patch) with:
|
||||
- title: title of the commit message (first line)
|
||||
|
|
|
|||
|
|
@ -348,9 +348,8 @@ class TextSyntaxes(object):
|
|||
element.text = ''
|
||||
return html.tostring(xhtml_elt, encoding=str, method="xml")
|
||||
|
||||
def convert(
|
||||
self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True, profile=None
|
||||
):
|
||||
def convert(self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True,
|
||||
profile=None):
|
||||
"""Convert a text between two syntaxes
|
||||
|
||||
@param text: text to convert
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@
|
|||
from sat.core.i18n import _
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.log import getLogger
|
||||
|
||||
log = getLogger(__name__)
|
||||
from twisted.words.xish import domish
|
||||
from functools import partial
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
PLUGIN_INFO = {
|
||||
C.PI_NAME: "Raw XML log Plugin",
|
||||
C.PI_IMPORT_NAME: "XmlLog",
|
||||
|
|
@ -74,9 +74,9 @@ class XmlLog(object):
|
|||
|
||||
def onSend(self, obj, client):
|
||||
if isinstance(obj, str):
|
||||
log = str(obj)
|
||||
xml_log = obj
|
||||
elif isinstance(obj, domish.Element):
|
||||
log = obj.toXml()
|
||||
xml_log = obj.toXml()
|
||||
else:
|
||||
log.error(_("INTERNAL ERROR: Unmanaged XML type"))
|
||||
self.host.bridge.xmlLog("OUT", log, client.profile)
|
||||
self.host.bridge.xmlLog("OUT", xml_log, client.profile)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class Context(potr.context.Context):
|
|||
message data when an encrypted message is going to be sent
|
||||
"""
|
||||
assert isinstance(self.peer, jid.JID)
|
||||
msg = msg_str
|
||||
msg = msg_str.decode('utf-8')
|
||||
client = self.user.client
|
||||
log.debug("injecting encrypted message to {to}".format(to=self.peer))
|
||||
if appdata is None:
|
||||
|
|
@ -530,7 +530,7 @@ class OTR(object):
|
|||
) # FIXME: temporary and unsecure, must be changed when frontends
|
||||
# are refactored
|
||||
otrctx = client._otr_context_manager.getContextForUser(to_jid)
|
||||
query = otrctx.sendMessage(0, "?OTRv?")
|
||||
query = otrctx.sendMessage(0, b"?OTRv?")
|
||||
otrctx.inject(query)
|
||||
|
||||
def _otrSessionEnd(self, menu_data, profile):
|
||||
|
|
|
|||
|
|
@ -241,12 +241,12 @@ class XEP_0048(object):
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
for url in urls_data:
|
||||
for url, url_data in urls_data.items():
|
||||
url_elt = storage_elt.addElement("url")
|
||||
url_elt[XEP_0048.URL_KEY] = url
|
||||
for attr in XEP_0048.URL_ATTRS:
|
||||
try:
|
||||
url_elt[attr] = url[attr]
|
||||
url_elt[attr] = url_data[attr]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
|
@ -440,7 +440,10 @@ class XEP_0048(object):
|
|||
return ret
|
||||
data = bookmarks_ori[type_]
|
||||
for bookmark in data:
|
||||
ret[_storage_location][bookmark.full()] = data[bookmark].copy()
|
||||
if type_ == XEP_0048.MUC_TYPE:
|
||||
ret[_storage_location][bookmark.full()] = data[bookmark].copy()
|
||||
else:
|
||||
ret[_storage_location][bookmark] = data[bookmark].copy()
|
||||
return ret
|
||||
|
||||
for _storage_location in ("local", "private", "pubsub"):
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ except:
|
|||
raise exceptions.MissingModule(
|
||||
"Missing module pillow, please download/install it from https://python-pillow.github.io"
|
||||
)
|
||||
from io import StringIO
|
||||
import io
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
|
|
@ -52,7 +52,8 @@ except ImportError:
|
|||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
AVATAR_PATH = "avatars"
|
||||
AVATAR_DIM = (64, 64) # FIXME: dim are not adapted to modern resolutions !
|
||||
# AVATAR_DIM = (64, 64) # FIXME: dim are not adapted to modern resolutions !
|
||||
AVATAR_DIM = (128, 128)
|
||||
|
||||
IQ_GET = '/iq[@type="get"]'
|
||||
NS_VCARD = "vcard-temp"
|
||||
|
|
@ -318,7 +319,7 @@ class XEP_0054(object):
|
|||
avatar_hash = yield threads.deferToThread(
|
||||
self.savePhoto, client, elem, entity_jid
|
||||
)
|
||||
except (exceptions.DataError, exceptions.NotFound) as e:
|
||||
except (exceptions.DataError, exceptions.NotFound):
|
||||
avatar_hash = ""
|
||||
vcard_dict["avatar"] = avatar_hash
|
||||
except Exception as e:
|
||||
|
|
@ -515,12 +516,13 @@ class XEP_0054(object):
|
|||
left += offset
|
||||
right -= offset
|
||||
img = img.crop((left, upper, right, lower))
|
||||
img_buf = StringIO()
|
||||
img_buf = io.BytesIO()
|
||||
img.save(img_buf, "PNG")
|
||||
|
||||
photo_elt = vcard_elt.addElement("PHOTO")
|
||||
photo_elt.addElement("TYPE", content="image/png")
|
||||
photo_elt.addElement("BINVAL", content=b64encode(img_buf.getvalue()))
|
||||
image_b64 = b64encode(img_buf.getvalue()).decode('utf-8')
|
||||
photo_elt.addElement("BINVAL", content=image_b64)
|
||||
image_hash = sha1(img_buf.getvalue()).hexdigest()
|
||||
with client.cache.cacheData(
|
||||
PLUGIN_INFO["import_name"], image_hash, "image/png", MAX_AGE
|
||||
|
|
|
|||
|
|
@ -175,17 +175,8 @@ ProxyInfos = namedtuple("ProxyInfos", ["host", "jid", "port"])
|
|||
|
||||
|
||||
class Candidate(object):
|
||||
def __init__(
|
||||
self,
|
||||
host,
|
||||
port,
|
||||
type_,
|
||||
priority,
|
||||
jid_,
|
||||
id_=None,
|
||||
priority_local=False,
|
||||
factory=None,
|
||||
):
|
||||
def __init__(self, host, port, type_, priority, jid_, id_=None, priority_local=False,
|
||||
factory=None,):
|
||||
"""
|
||||
@param host(unicode): host IP or domain
|
||||
@param port(int): port
|
||||
|
|
@ -227,15 +218,6 @@ class Candidate(object):
|
|||
return self._priority
|
||||
|
||||
def __str__(self):
|
||||
# similar to __unicode__ but we don't show jid and we encode id
|
||||
return "Candidate ({0.priority}): host={0.host} port={0.port} type={0.type}{id}".format(
|
||||
self,
|
||||
id=" id={}".format(self.id if self.id is not None else "").encode(
|
||||
"utf-8", "ignore"
|
||||
),
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return "Candidate ({0.priority}): host={0.host} port={0.port} jid={0.jid} type={0.type}{id}".format(
|
||||
self, id=" id={}".format(self.id if self.id is not None else "")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -113,9 +113,11 @@ class XEP_0166(object):
|
|||
try:
|
||||
del client.jingle_sessions[sid]
|
||||
except KeyError:
|
||||
log.debug("Jingle session id [{}] is unknown, nothing to delete".format(sid))
|
||||
log.debug(
|
||||
f"Jingle session id {sid!r} is unknown, nothing to delete "
|
||||
f"[{client.profile}]")
|
||||
else:
|
||||
log.debug("Jingle session id [{}] deleted".format(sid))
|
||||
log.debug(f"Jingle session id {sid!r} deleted [{client.profile}]")
|
||||
|
||||
## helpers methods to build stanzas ##
|
||||
|
||||
|
|
@ -146,7 +148,7 @@ class XEP_0166(object):
|
|||
condition=error_condition
|
||||
)
|
||||
)
|
||||
client.send(iq_elt)
|
||||
return client.send(iq_elt)
|
||||
|
||||
def _terminateEb(self, failure_):
|
||||
log.warning(_("Error while terminating session: {msg}").format(msg=failure_))
|
||||
|
|
@ -188,23 +190,22 @@ class XEP_0166(object):
|
|||
)
|
||||
self._delSession(client, sid)
|
||||
|
||||
def _jingleErrorCb(self, fail, sid, request, client):
|
||||
def _jingleErrorCb(self, failure_, session, request, client):
|
||||
"""Called when something is going wrong while parsing jingle request
|
||||
|
||||
The error condition depend of the exceptions raised:
|
||||
exceptions.DataError raise a bad-request condition
|
||||
@param fail(failure.Failure): the exceptions raised
|
||||
@param sid(unicode): jingle session id
|
||||
@param session(dict): data of the session
|
||||
@param request(domsih.Element): jingle request
|
||||
@param client: %(doc_client)s
|
||||
"""
|
||||
log.warning("Error while processing jingle request")
|
||||
if isinstance(fail, exceptions.DataError):
|
||||
self.sendError(client, "bad-request", sid, request)
|
||||
log.warning(f"Error while processing jingle request [{client.profile}]")
|
||||
if isinstance(failure_, exceptions.DataError):
|
||||
return self.sendError(client, "bad-request", session['id'], request)
|
||||
else:
|
||||
log.error("Unmanaged jingle exception")
|
||||
self._delSession(client, sid)
|
||||
raise fail
|
||||
log.error(f"Unmanaged jingle exception: {failure_}")
|
||||
return self.terminate(client, self.REASON_FAILED_APPLICATION, session)
|
||||
|
||||
## methods used by other plugins ##
|
||||
|
||||
|
|
@ -706,19 +707,10 @@ class XEP_0166(object):
|
|||
"""
|
||||
return elt
|
||||
|
||||
def _callPlugins(
|
||||
self,
|
||||
client,
|
||||
action,
|
||||
session,
|
||||
app_method_name="jingleHandler",
|
||||
transp_method_name="jingleHandler",
|
||||
app_default_cb=None,
|
||||
transp_default_cb=None,
|
||||
delete=True,
|
||||
elements=True,
|
||||
force_element=None,
|
||||
):
|
||||
def _callPlugins(self, client, action, session, app_method_name="jingleHandler",
|
||||
transp_method_name="jingleHandler", app_default_cb=None,
|
||||
transp_default_cb=None, delete=True, elements=True,
|
||||
force_element=None):
|
||||
"""Call application and transport plugin methods for all contents
|
||||
|
||||
@param action(unicode): jingle action name
|
||||
|
|
@ -819,7 +811,7 @@ class XEP_0166(object):
|
|||
|
||||
confirm_dlist = defer.gatherResults(confirm_defers)
|
||||
confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client)
|
||||
confirm_dlist.addErrback(self._jingleErrorCb, session["id"], request, client)
|
||||
confirm_dlist.addErrback(self._jingleErrorCb, session, request, client)
|
||||
|
||||
def _confirmationCb(self, confirm_results, session, jingle_elt, client):
|
||||
"""Method called when confirmation from user has been received
|
||||
|
|
@ -952,7 +944,7 @@ class XEP_0166(object):
|
|||
negociate_defers = []
|
||||
negociate_defers = self._callPlugins(client, XEP_0166.A_SESSION_ACCEPT, session)
|
||||
|
||||
negociate_dlist = defer.DeferredList(negociate_defers)
|
||||
negociate_dlist = defer.gatherResults(negociate_defers)
|
||||
|
||||
# after negociations we start the transfer
|
||||
negociate_dlist.addCallback(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ PLUGIN_INFO = {
|
|||
C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
|
||||
}
|
||||
|
||||
EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash"}
|
||||
EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
|
||||
Range = namedtuple("Range", ("offset", "length"))
|
||||
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ class XEP_0234(object):
|
|||
|
||||
# generic methods
|
||||
|
||||
def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None,
|
||||
def buildFileElement(self, name=None, file_hash=None, hash_algo=None, size=None,
|
||||
mime_type=None, desc=None, modified=None, transfer_range=None, path=None,
|
||||
namespace=None, file_elt=None, **kwargs):
|
||||
"""Generate a <file> element with available metadata
|
||||
|
|
@ -177,14 +177,8 @@ class XEP_0234(object):
|
|||
file_data.update(kwargs)
|
||||
return self.buildFileElement(**file_data)
|
||||
|
||||
def parseFileElement(
|
||||
self,
|
||||
file_elt,
|
||||
file_data=None,
|
||||
given=False,
|
||||
parent_elt=None,
|
||||
keep_empty_range=False,
|
||||
):
|
||||
def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None,
|
||||
keep_empty_range=False,):
|
||||
"""Parse a <file> element and file dictionary accordingly
|
||||
|
||||
@param file_data(dict, None): dict where the data will be set
|
||||
|
|
@ -194,10 +188,12 @@ class XEP_0234(object):
|
|||
@param given(bool): if True, prefix hash key with "given_"
|
||||
@param parent_elt(domish.Element, None): parent of the file element
|
||||
if set, file_elt must not be set
|
||||
@param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None)
|
||||
empty range are useful to know if a peer_jid can handle range
|
||||
@param keep_empty_range(bool): if True, keep empty range (i.e. range when offset
|
||||
and length are None).
|
||||
Empty range is useful to know if a peer_jid can handle range
|
||||
@return (dict): file_data
|
||||
@trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements
|
||||
@trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new
|
||||
elements
|
||||
@raise exceptions.NotFound: there is not <file> element in parent_elt
|
||||
@raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
|
||||
"""
|
||||
|
|
@ -230,7 +226,7 @@ class XEP_0234(object):
|
|||
# we don't want to go to parent dir when joining to a path
|
||||
name = "--"
|
||||
file_data["name"] = name
|
||||
elif name is not None and "/" in name or "\\" in name:
|
||||
elif name is not None and ("/" in name or "\\" in name):
|
||||
file_data["name"] = regex.pathEscape(name)
|
||||
|
||||
try:
|
||||
|
|
@ -335,15 +331,8 @@ class XEP_0234(object):
|
|||
defer.returnValue(progress_id)
|
||||
|
||||
def _fileJingleRequest(
|
||||
self,
|
||||
peer_jid,
|
||||
filepath,
|
||||
name="",
|
||||
file_hash="",
|
||||
hash_algo="",
|
||||
extra=None,
|
||||
profile=C.PROF_KEY_NONE,
|
||||
):
|
||||
self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
|
||||
profile=C.PROF_KEY_NONE):
|
||||
client = self.host.getClient(profile)
|
||||
return self.fileJingleRequest(
|
||||
client,
|
||||
|
|
@ -357,15 +346,8 @@ class XEP_0234(object):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def fileJingleRequest(
|
||||
self,
|
||||
client,
|
||||
peer_jid,
|
||||
filepath,
|
||||
name=None,
|
||||
file_hash=None,
|
||||
hash_algo=None,
|
||||
extra=None,
|
||||
):
|
||||
self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None,
|
||||
extra=None):
|
||||
"""Request a file using jingle file transfer
|
||||
|
||||
@param peer_jid(jid.JID): destinee jid
|
||||
|
|
@ -568,6 +550,7 @@ class XEP_0234(object):
|
|||
d.addCallback(gotConfirmation)
|
||||
return d
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def jingleHandler(self, client, action, session, content_name, desc_elt):
|
||||
content_data = session["contents"][content_name]
|
||||
application_data = content_data["application_data"]
|
||||
|
|
@ -579,7 +562,7 @@ class XEP_0234(object):
|
|||
next(file_elt.elements(NS_JINGLE_FT, "range"))
|
||||
except StopIteration:
|
||||
# initiator doesn't manage <range>, but we do so we advertise it
|
||||
# FIXME: to be checked
|
||||
# FIXME: to be checked
|
||||
log.debug("adding <range> element")
|
||||
file_elt.addElement("range")
|
||||
elif action == self._j.A_SESSION_ACCEPT:
|
||||
|
|
@ -596,21 +579,32 @@ class XEP_0234(object):
|
|||
size = int(str(size_elt))
|
||||
except (StopIteration, ValueError):
|
||||
size = None
|
||||
# XXX: hash security is not critical here, so we just take the higher mandatory one
|
||||
# XXX: hash security is not critical here, so we just take the higher
|
||||
# mandatory one
|
||||
hasher = file_data["hash_hasher"] = self._hash.getHasher()
|
||||
content_data["stream_object"] = stream.FileStreamObject(
|
||||
self.host,
|
||||
client,
|
||||
file_path,
|
||||
mode="wb",
|
||||
uid=self.getProgressId(session, content_name),
|
||||
size=size,
|
||||
data_cb=lambda data: hasher.update(data),
|
||||
)
|
||||
progress_id = self.getProgressId(session, content_name)
|
||||
try:
|
||||
content_data["stream_object"] = stream.FileStreamObject(
|
||||
self.host,
|
||||
client,
|
||||
file_path,
|
||||
mode="wb",
|
||||
uid=progress_id,
|
||||
size=size,
|
||||
data_cb=lambda data: hasher.update(data),
|
||||
)
|
||||
except Exception as e:
|
||||
self.host.bridge.progressError(
|
||||
progress_id, C.PROGRESS_ERROR_FAILED, client.profile
|
||||
)
|
||||
yield self._j.terminate(
|
||||
client, self._j.REASON_FAILED_APPLICATION, session)
|
||||
raise e
|
||||
else:
|
||||
# we are sending the file
|
||||
size = file_data["size"]
|
||||
# XXX: hash security is not critical here, so we just take the higher mandatory one
|
||||
# XXX: hash security is not critical here, so we just take the higher
|
||||
# mandatory one
|
||||
hasher = file_data["hash_hasher"] = self._hash.getHasher()
|
||||
content_data["stream_object"] = stream.FileStreamObject(
|
||||
self.host,
|
||||
|
|
@ -625,7 +619,7 @@ class XEP_0234(object):
|
|||
finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
|
||||
else:
|
||||
log.warning("FIXME: unmanaged action {}".format(action))
|
||||
return desc_elt
|
||||
defer.returnValue(desc_elt)
|
||||
|
||||
def jingleSessionInfo(self, client, action, session, content_name, jingle_elt):
|
||||
"""Called on session-info action
|
||||
|
|
@ -679,6 +673,16 @@ class XEP_0234(object):
|
|||
self.host.bridge.progressError(
|
||||
progress_id, C.PROGRESS_ERROR_DECLINED, client.profile
|
||||
)
|
||||
elif not jingle_elt.success:
|
||||
progress_id = self.getProgressId(session, content_name)
|
||||
first_child = jingle_elt.firstChildElement()
|
||||
if first_child is not None:
|
||||
reason = first_child.name
|
||||
else:
|
||||
reason = C.PROGRESS_ERROR_FAILED
|
||||
self.host.bridge.progressError(
|
||||
progress_id, reason, client.profile
|
||||
)
|
||||
|
||||
def _sendCheckSum(self, client, session, content_name, content_data):
|
||||
"""Send the session-info with the hash checksum"""
|
||||
|
|
@ -721,10 +725,10 @@ class XEP_0234(object):
|
|||
return True
|
||||
return False
|
||||
hasher = file_data["hash_hasher"]
|
||||
hash_ = hasher.hexdigest().encode('utf-8')
|
||||
hash_ = hasher.hexdigest()
|
||||
|
||||
if hash_ == given_hash:
|
||||
log.info("Hash checked, file was successfully transfered: {}".format(hash_))
|
||||
log.info(f"Hash checked, file was successfully transfered: {hash_}")
|
||||
progress_metadata = {
|
||||
"hash": hash_,
|
||||
"hash_algo": file_data["hash_algo"],
|
||||
|
|
|
|||
|
|
@ -196,13 +196,13 @@ class XEP_0300(object):
|
|||
try:
|
||||
idx = algos.index(algo)
|
||||
except ValueError:
|
||||
log.warning("Proposed {} algorithm is not managed".format(algo))
|
||||
log.warning(f"Proposed {algo} algorithm is not managed")
|
||||
algo = None
|
||||
continue
|
||||
|
||||
if best_algo is None or algos.index(best_algo) < idx:
|
||||
best_algo = algo
|
||||
best_value = base64.b64decode(str(hash_elt))
|
||||
best_value = base64.b64decode(str(hash_elt)).decode('utf-8')
|
||||
|
||||
if not hash_elt:
|
||||
raise exceptions.NotFound
|
||||
|
|
|
|||
|
|
@ -595,10 +595,14 @@ class XEP_0329(object):
|
|||
):
|
||||
return
|
||||
for file_data in files_data:
|
||||
file_elt = self._jf.buildFileElementFromDict(
|
||||
file_data, modified=file_data.get("modified", file_data["created"])
|
||||
)
|
||||
query_elt.addChild(file_elt)
|
||||
if file_data['type'] == C.FILE_TYPE_DIRECTORY:
|
||||
directory_elt = query_elt.addElement("directory")
|
||||
directory_elt['name'] = file_data['name']
|
||||
else:
|
||||
file_elt = self._jf.buildFileElementFromDict(
|
||||
file_data, modified=file_data.get("modified", file_data["created"])
|
||||
)
|
||||
query_elt.addChild(file_elt)
|
||||
client.send(iq_result_elt)
|
||||
|
||||
def onComponentRequest(self, iq_elt, client):
|
||||
|
|
@ -624,7 +628,7 @@ class XEP_0329(object):
|
|||
file_data = {"name": elt["name"], "type": C.FILE_TYPE_DIRECTORY}
|
||||
else:
|
||||
log.warning(
|
||||
_("unexpected element, ignoring: {elt}").format(elt=elt.toXml())
|
||||
_(f"unexpected element, ignoring: {elt.toXml()}")
|
||||
)
|
||||
continue
|
||||
files.append(file_data)
|
||||
|
|
@ -642,7 +646,7 @@ class XEP_0329(object):
|
|||
|
||||
def _listFiles(self, target_jid, path, extra, profile):
|
||||
client = self.host.getClient(profile)
|
||||
target_jid = client.jid.userhostJID() if not target_jid else jid.JID(target_jid)
|
||||
target_jid = client.jid if not target_jid else jid.JID(target_jid)
|
||||
d = self.listFiles(client, target_jid, path or None)
|
||||
d.addCallback(self._serializeData)
|
||||
return d
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ from twisted.web import http_headers
|
|||
from twisted.web import iweb
|
||||
from twisted.python import failure
|
||||
from collections import namedtuple
|
||||
from zope.interface import implementer
|
||||
from OpenSSL import SSL
|
||||
import os.path
|
||||
import mimetypes
|
||||
|
|
@ -204,7 +203,7 @@ class XEP_0363(object):
|
|||
@param ignore_tls_errors(bool): ignore TLS certificate is True
|
||||
@return (tuple
|
||||
"""
|
||||
log.debug("Got upload slot: {}".format(slot))
|
||||
log.debug(f"Got upload slot: {slot}")
|
||||
sat_file = self.host.plugins["FILE"].File(
|
||||
self.host, client, path, size=size, auto_end_signals=False
|
||||
)
|
||||
|
|
@ -222,7 +221,7 @@ class XEP_0363(object):
|
|||
headers[name] = value
|
||||
|
||||
d = agent.request(
|
||||
"PUT",
|
||||
b"PUT",
|
||||
slot.put.encode("utf-8"),
|
||||
http_headers.Headers(headers),
|
||||
file_producer,
|
||||
|
|
|
|||
|
|
@ -76,8 +76,7 @@ class CommandProtocol(protocol.ProcessProtocol):
|
|||
def processEnded(self, reason):
|
||||
data = b''.join(self.data)
|
||||
if (reason.value.exitCode == 0):
|
||||
log.debug(_('{name!r} command succeed').format(
|
||||
name=self.command_name.decode('utf-8', 'ignore')))
|
||||
log.debug(_(f'{self.command_name!r} command succeed'))
|
||||
# we don't use "replace" on purpose, we want an exception if decoding
|
||||
# is not working properly
|
||||
self._deferred.callback(data)
|
||||
|
|
@ -106,7 +105,7 @@ class CommandProtocol(protocol.ProcessProtocol):
|
|||
- stdin(unicode, None): data to push to standard input
|
||||
- verbose(bool): if True stdout and stderr will be logged
|
||||
other keyword arguments will be used in reactor.spawnProcess
|
||||
@return ((D)): stdout in case of success
|
||||
@return ((D)bytes): stdout in case of success
|
||||
@raise RuntimeError: command returned a non zero status
|
||||
stdin and stdout will be given as arguments
|
||||
|
||||
|
|
@ -115,8 +114,6 @@ class CommandProtocol(protocol.ProcessProtocol):
|
|||
if stdin is not None:
|
||||
stdin = stdin.encode('utf-8')
|
||||
verbose = kwargs.pop('verbose', False)
|
||||
if 'path' in kwargs:
|
||||
kwargs['path'] = kwargs['path'].encode('utf-8')
|
||||
args = [a.encode('utf-8') for a in args]
|
||||
kwargs = {k:v.encode('utf-8') for k,v in list(kwargs.items())}
|
||||
d = defer.Deferred()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -39,7 +39,6 @@ class AccountCreate(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("create a XMPP account"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -72,80 +71,81 @@ class AccountCreate(base.CommandBase):
|
|||
"--port",
|
||||
type=int,
|
||||
default=0,
|
||||
help=_("server port (IP address or domain, default: use localhost)"),
|
||||
help=_(f"server port (default: {C.XMPP_C2S_PORT})"),
|
||||
)
|
||||
|
||||
def _setParamCb(self):
|
||||
self.host.bridge.setParam(
|
||||
"Password",
|
||||
self.args.password,
|
||||
"Connection",
|
||||
profile_key=self.args.profile,
|
||||
callback=self.host.quit,
|
||||
errback=self.errback,
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
await self.host.bridge.inBandAccountNew(
|
||||
self.args.jid,
|
||||
self.args.password,
|
||||
self.args.email,
|
||||
self.args.host,
|
||||
self.args.port,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
f"can't create account on {self.args.host or 'localhost'!r} with jid "
|
||||
f"{self.args.jid!r} using In-Band Registration: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def _session_started(self, __):
|
||||
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(_("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(
|
||||
_(
|
||||
"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(_("XMPP account created"), 1)
|
||||
if self.args.profile is not None:
|
||||
self.disp(_("creating profile"), 2)
|
||||
self.host.bridge.profileCreate(
|
||||
|
||||
if self.args.profile is None:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
self.disp(_("creating profile"), 2)
|
||||
try:
|
||||
await self.host.bridge.profileCreate(
|
||||
self.args.profile,
|
||||
self.args.password,
|
||||
"",
|
||||
callback=self._profileCreateCb,
|
||||
errback=self._profileCreateEb,
|
||||
)
|
||||
else:
|
||||
self.host.quit()
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
_(f"Can't create profile {self.args.profile} to associate with jid "
|
||||
f"{self.args.jid}: {e}"),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
self.disp(_("profile created"), 1)
|
||||
try:
|
||||
await self.host.bridge.profileStartSession(
|
||||
self.args.password,
|
||||
self.args.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't start profile session: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
await self.host.bridge.setParam(
|
||||
"JabberID",
|
||||
self.args.jid,
|
||||
"Connection",
|
||||
profile_key=self.args.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set JabberID parameter: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
await self.host.bridge.setParam(
|
||||
"Password",
|
||||
self.args.password,
|
||||
"Connection",
|
||||
profile_key=self.args.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set Password parameter: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def accountNewEb(self, failure_):
|
||||
self.disp(
|
||||
_("Can't create new account on server {host} with jid {jid}: {msg}").format(
|
||||
host=self.args.host or "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,
|
||||
)
|
||||
f"profile {self.args.profile} successfully created and associated to the new "
|
||||
f"account", 1)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class AccountModify(base.CommandBase):
|
||||
|
|
@ -153,20 +153,23 @@ class AccountModify(base.CommandBase):
|
|||
super(AccountModify, self).__init__(
|
||||
host, "modify", help=_("change password for XMPP account")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
"password", help=_("new XMPP password")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.inBandPasswordChange(
|
||||
self.args.password,
|
||||
self.args.profile,
|
||||
callback=self.host.quit,
|
||||
errback=self.errback,
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
await self.host.bridge.inBandPasswordChange(
|
||||
self.args.password,
|
||||
self.args.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't change XMPP password: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class AccountDelete(base.CommandBase):
|
||||
|
|
@ -174,7 +177,6 @@ class AccountDelete(base.CommandBase):
|
|||
super(AccountDelete, self).__init__(
|
||||
host, "delete", help=_("delete a XMPP account")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -184,32 +186,33 @@ class AccountDelete(base.CommandBase):
|
|||
help=_("delete account without confirmation"),
|
||||
)
|
||||
|
||||
def _got_jid(self, jid_str):
|
||||
async def start(self):
|
||||
try:
|
||||
jid_str = await self.host.bridge.asyncGetParamA(
|
||||
"JabberID",
|
||||
"Connection",
|
||||
profile_key=self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get JID of the profile: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
jid_ = jid.JID(jid_str)
|
||||
if not self.args.force:
|
||||
message = (
|
||||
"You are about to delete the XMPP account with jid {jid_}\n"
|
||||
'This is the XMPP account of profile "{profile}"\n'
|
||||
"Are you sure that you want to delete this account ?".format(
|
||||
jid_=jid_, profile=self.profile
|
||||
)
|
||||
f"You are about to delete the XMPP account with jid {jid_!r}\n"
|
||||
f"This is the XMPP account of profile {self.profile!r}\n"
|
||||
f"Are you sure that you want to delete this account?"
|
||||
)
|
||||
res = input("{} (y/N)? ".format(message))
|
||||
if res not in ("y", "Y"):
|
||||
self.disp(_("Account deletion cancelled"))
|
||||
self.host.quit(2)
|
||||
self.host.bridge.inBandUnregister(
|
||||
jid_.domain, self.args.profile, callback=self.host.quit, errback=self.errback
|
||||
)
|
||||
await self.host.confirmOrQuit(message, _("Account deletion cancelled"))
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.asyncGetParamA(
|
||||
"JabberID",
|
||||
"Connection",
|
||||
profile_key=self.profile,
|
||||
callback=self._got_jid,
|
||||
errback=self.errback,
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.inBandUnregister(jid_.domain, self.args.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't delete XMPP account with jid {jid_!r}: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Account(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
from . 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
|
||||
|
||||
|
|
@ -67,7 +66,7 @@ class Remote(base.CommandBase):
|
|||
"-l", "--loop", action="store_true", help=_("loop on the commands")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
name = self.args.software.lower()
|
||||
flags = []
|
||||
magics = {jid for jid in self.args.jids if jid.count("@") > 1}
|
||||
|
|
@ -75,29 +74,33 @@ class Remote(base.CommandBase):
|
|||
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,
|
||||
try:
|
||||
bus_name, methods = await self.host.bridge.adHocDBusAddAuto(
|
||||
name,
|
||||
list(jids),
|
||||
self.args.groups,
|
||||
magics,
|
||||
self.args.forbidden_jids,
|
||||
self.args.forbidden_groups,
|
||||
flags,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create remote control: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
if not bus_name:
|
||||
self.disp(_("No bus name found"), 1)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
else:
|
||||
self.disp(_("Bus name found: [%s]" % bus_name), 1)
|
||||
for method in methods:
|
||||
path, iface, command = method
|
||||
self.disp(
|
||||
_(f"Command found: (path:{path}, iface: {iface}) [{command}]"),
|
||||
1,
|
||||
)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Run(base.CommandBase):
|
||||
|
|
@ -107,7 +110,6 @@ class Run(base.CommandBase):
|
|||
super(Run, self).__init__(
|
||||
host, "run", use_verbose=True, help=_("run an Ad-Hoc command")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -140,28 +142,24 @@ class Run(base.CommandBase):
|
|||
help=_("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=_("can't get ad-hoc commands list: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
xmlui_raw = await self.host.bridge.adHocRun(
|
||||
self.args.jid,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get ad-hoc commands list: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
xmlui = xmlui_manager.create(self.host, xmlui_raw)
|
||||
workflow = self.args.workflow
|
||||
await xmlui.show(workflow)
|
||||
if not workflow:
|
||||
if xmlui.type == "form":
|
||||
await xmlui.submitForm()
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class List(base.CommandBase):
|
||||
|
|
@ -171,7 +169,6 @@ class List(base.CommandBase):
|
|||
super(List, self).__init__(
|
||||
host, "list", use_verbose=True, help=_("list Ad-Hoc commands of a service")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -181,23 +178,19 @@ class List(base.CommandBase):
|
|||
help=_("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=_("can't get ad-hoc commands list: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
xmlui_raw = await self.host.bridge.adHocList(
|
||||
self.args.jid,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get ad-hoc commands list: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
xmlui = xmlui_manager.create(self.host, xmlui_raw)
|
||||
await xmlui.show(read_only=True)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class AdHoc(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -18,59 +18,24 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from . import base
|
||||
import os
|
||||
import os.path
|
||||
import asyncio
|
||||
from . import base
|
||||
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(_("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", help=_("entity"))
|
||||
|
|
@ -78,54 +43,81 @@ class Get(base.CommandBase):
|
|||
"-s", "--show", action="store_true", help=_("show avatar")
|
||||
)
|
||||
|
||||
def showImage(self, path):
|
||||
async 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])
|
||||
process = await asyncio.create_subprocess_exec(cmd, path)
|
||||
ret = await process.wait()
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
if ret in (0, 2):
|
||||
# we can get exit code 2 with display when stopping it with C-c
|
||||
break
|
||||
continue
|
||||
|
||||
if ret in (0, 2):
|
||||
# we can get exit code 2 with display when stopping it with C-c
|
||||
break
|
||||
else:
|
||||
# didn't worked with commands, we try our luck with webbrowser
|
||||
# in some cases, webbrowser can actually open the associated display program
|
||||
# in some cases, webbrowser can actually open the associated display program.
|
||||
# Note that this may be possibly blocking, depending on the platform and
|
||||
# available browser
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(path)
|
||||
|
||||
def _avatarGetCb(self, avatar_path):
|
||||
async def start(self):
|
||||
try:
|
||||
avatar_path = await self.host.bridge.avatarGet(
|
||||
self.args.jid,
|
||||
False,
|
||||
False,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't retrieve avatar: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
if not avatar_path:
|
||||
self.disp(_("No avatar found."), 1)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
|
||||
self.disp(avatar_path)
|
||||
if self.args.show:
|
||||
self.showImage(avatar_path)
|
||||
await 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 Set(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(Set, self).__init__(
|
||||
host, "set", use_verbose=True, help=_("set avatar of the profile")
|
||||
)
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
"image_path", type=str, help=_("path to the image to upload")
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
path = self.args.image_path
|
||||
if not os.path.exists(path):
|
||||
self.disp(_(f"file {path!r} doesn't exist!"), error=True)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
path = os.path.abspath(path)
|
||||
try:
|
||||
await self.host.bridge.avatarSet(path, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set avatar: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("avatar has been set"), 1)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Avatar(base.CommandBase):
|
||||
subcommands = (Set, Get)
|
||||
subcommands = (Get, Set)
|
||||
|
||||
def __init__(self, host):
|
||||
super(Avatar, self).__init__(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os.path
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
import subprocess
|
||||
import asyncio
|
||||
from asyncio.subprocess import DEVNULL
|
||||
from pathlib import Path
|
||||
from . import base
|
||||
from sat.core.i18n import _
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
|
|
@ -27,15 +37,6 @@ 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"]
|
||||
|
|
@ -64,7 +65,7 @@ KEY_TO_REMOVE_METADATA = (
|
|||
)
|
||||
|
||||
URL_REDIRECT_PREFIX = "url_redirect_"
|
||||
INOTIFY_INSTALL = '"pip install inotify"'
|
||||
AIONOTIFY_INSTALL = '"pip install aionotify"'
|
||||
MB_KEYS = (
|
||||
"id",
|
||||
"url",
|
||||
|
|
@ -86,7 +87,7 @@ MB_KEYS = (
|
|||
OUTPUT_OPT_NO_HEADER = "no-header"
|
||||
|
||||
|
||||
def guessSyntaxFromPath(host, sat_conf, path):
|
||||
async def guessSyntaxFromPath(host, sat_conf, path):
|
||||
"""Return syntax guessed according to filename extension
|
||||
|
||||
@param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
|
||||
|
|
@ -101,19 +102,35 @@ def guessSyntaxFromPath(host, sat_conf, path):
|
|||
return k
|
||||
|
||||
# if not found, we use current syntax
|
||||
return host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
|
||||
return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
|
||||
|
||||
|
||||
class BlogPublishCommon(object):
|
||||
"""handle common option for publising commands (Set and Edit)"""
|
||||
|
||||
@property
|
||||
def current_syntax(self):
|
||||
if self._current_syntax is None:
|
||||
self._current_syntax = self.host.bridge.getParamA(
|
||||
async def get_current_syntax(self):
|
||||
"""Retrieve current_syntax
|
||||
|
||||
Use default syntax if --syntax has not been used, else check given syntax.
|
||||
Will set self.default_syntax_used to True if default syntax has been used
|
||||
"""
|
||||
if self.args.syntax is None:
|
||||
self.default_syntax_used = True
|
||||
return await self.host.bridge.getParamA(
|
||||
"Syntax", "Composition", "value", self.profile
|
||||
)
|
||||
return self._current_syntax
|
||||
else:
|
||||
self.default_syntax_used = False
|
||||
try:
|
||||
syntax = await self.host.bridge.syntaxGet(self.current_syntax)
|
||||
|
||||
self.current_syntax = self.args.syntax = syntax
|
||||
except Exception as e:
|
||||
if e.classname == "NotFound":
|
||||
self.parser.error(_(f"unknown syntax requested ({self.args.syntax})"))
|
||||
else:
|
||||
raise e
|
||||
return self.args.syntax
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -143,14 +160,14 @@ class BlogPublishCommon(object):
|
|||
help=_("syntax to use (default: get profile's default syntax)"),
|
||||
)
|
||||
|
||||
def setMbDataContent(self, content, mb_data):
|
||||
if self.args.syntax is None:
|
||||
async def setMbDataContent(self, content, mb_data):
|
||||
if self.default_syntax_used:
|
||||
# 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(
|
||||
mb_data["content_xhtml"] = await self.host.bridge.syntaxConvert(
|
||||
content, self.current_syntax, SYNTAX_XHTML, False, self.profile
|
||||
)
|
||||
|
||||
|
|
@ -178,37 +195,35 @@ class Set(base.CommandBase, BlogPublishCommon):
|
|||
help=_("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("Item published")
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
def start(self):
|
||||
self._current_syntax = self.args.syntax
|
||||
async def start(self):
|
||||
self.current_syntax = await self.get_current_syntax()
|
||||
self.pubsub_item = self.args.item
|
||||
mb_data = {}
|
||||
self.setMbDataFromArgs(mb_data)
|
||||
if self.pubsub_item:
|
||||
mb_data["id"] = self.pubsub_item
|
||||
content = codecs.getreader("utf-8")(sys.stdin).read()
|
||||
self.setMbDataContent(content, mb_data)
|
||||
content = sys.stdin.read()
|
||||
await self.setMbDataContent(content, mb_data)
|
||||
|
||||
self.host.bridge.mbSend(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
data_format.serialise(mb_data),
|
||||
self.profile,
|
||||
callback=self.exitCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't send item: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.mbSend(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
data_format.serialise(mb_data),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
f"can't send item: {e}", error=True
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp("Item published")
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
|
|
@ -227,7 +242,6 @@ class Get(base.CommandBase):
|
|||
extra_outputs=extra_outputs,
|
||||
help=_("get blog item(s)"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
# TODO: a key(s) argument to select keys to display
|
||||
|
|
@ -401,28 +415,25 @@ class Get(base.CommandBase):
|
|||
|
||||
print(("\n" + sep + "\n"))
|
||||
|
||||
def mbGetCb(self, mb_result):
|
||||
items, metadata = mb_result
|
||||
items = [data_format.deserialise(i) for i in items]
|
||||
mb_result = items, metadata
|
||||
self.output(mb_result)
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
def mbGetEb(self, failure_):
|
||||
self.disp("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.getPubsubExtra(),
|
||||
self.profile,
|
||||
callback=self.mbGetCb,
|
||||
errback=self.mbGetEb,
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
mb_result = await self.host.bridge.mbGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.max,
|
||||
self.args.items,
|
||||
self.getPubsubExtra(),
|
||||
self.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get blog items: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
items, metadata = mb_result
|
||||
items = [data_format.deserialise(i) for i in items]
|
||||
mb_result = items, metadata
|
||||
await self.output(mb_result)
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
||||
|
|
@ -452,25 +463,27 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
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
|
||||
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
|
||||
@return (tuple[dict, Path]): 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):
|
||||
meta_file_path = content_file_path.with_name(
|
||||
content_file_path.stem + common.METADATA_SUFF)
|
||||
if meta_file_path.exists():
|
||||
self.disp("Metadata file already exists, we re-use it")
|
||||
try:
|
||||
with open(meta_file_path, "rb") as f:
|
||||
with meta_file_path.open("rb") as f:
|
||||
mb_data = json.load(f)
|
||||
except (OSError, IOError, ValueError) as e:
|
||||
self.disp(
|
||||
"Can't read existing metadata file at {path}, aborting: {reason}".format(
|
||||
path=meta_file_path, reason=e
|
||||
),
|
||||
f"Can't read existing metadata file at {meta_file_path}, "
|
||||
f"aborting: {e}",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
|
|
@ -491,7 +504,8 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
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
|
||||
# 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,
|
||||
|
|
@ -503,19 +517,20 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
|
||||
return mb_data, meta_file_path
|
||||
|
||||
def edit(self, content_file_path, content_file_obj, mb_data=None):
|
||||
async 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)
|
||||
|
||||
coroutines = []
|
||||
|
||||
# do we need a preview ?
|
||||
if self.args.preview:
|
||||
self.disp("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(
|
||||
[
|
||||
coroutines.append(
|
||||
asyncio.create_subprocess_exec(
|
||||
sys.argv[0],
|
||||
"blog",
|
||||
"preview",
|
||||
|
|
@ -523,30 +538,34 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
"true",
|
||||
"-p",
|
||||
self.profile,
|
||||
content_file_path,
|
||||
],
|
||||
stdout=DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
str(content_file_path),
|
||||
stdout=DEVNULL,
|
||||
stderr=DEVNULL,
|
||||
)
|
||||
)
|
||||
|
||||
# we launch editor
|
||||
self.runEditor(
|
||||
"blog_editor_args",
|
||||
content_file_path,
|
||||
content_file_obj,
|
||||
meta_file_path=meta_file_path,
|
||||
meta_ori=meta_ori,
|
||||
coroutines.append(
|
||||
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)
|
||||
await asyncio.gather(*coroutines)
|
||||
|
||||
async def publish(self, content, mb_data):
|
||||
await self.setMbDataContent(content, mb_data)
|
||||
|
||||
if self.pubsub_item:
|
||||
mb_data["id"] = self.pubsub_item
|
||||
|
||||
mb_data = data_format.serialise(mb_data)
|
||||
|
||||
self.host.bridge.mbSend(
|
||||
await self.host.bridge.mbSend(
|
||||
self.pubsub_service, self.pubsub_node, mb_data, self.profile
|
||||
)
|
||||
self.disp("Blog item published")
|
||||
|
|
@ -555,22 +574,27 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
# we get current syntax to determine file extension
|
||||
return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""])
|
||||
|
||||
def getItemData(self, service, node, item):
|
||||
async def getItemData(self, service, node, item):
|
||||
items = [item] if item else []
|
||||
mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0]
|
||||
mb_data = data_format.deserialise(mb_data)
|
||||
|
||||
mb_data = await self.host.bridge.mbGet(
|
||||
service, node, 1, items, {}, self.profile)
|
||||
mb_data = data_format.deserialise(mb_data[0][0])
|
||||
|
||||
try:
|
||||
content = mb_data["content_xhtml"]
|
||||
except KeyError:
|
||||
content = mb_data["content"]
|
||||
if content:
|
||||
content = self.host.bridge.syntaxConvert(
|
||||
content = await 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 = await self.host.bridge.syntaxConvert(
|
||||
content, SYNTAX_XHTML, self.current_syntax, False, self.profile
|
||||
)
|
||||
|
||||
if content and self.current_syntax == SYNTAX_XHTML:
|
||||
content = content.strip()
|
||||
if not content.startswith('<div>'):
|
||||
|
|
@ -586,37 +610,16 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
|
|||
|
||||
return content, mb_data, mb_data["id"]
|
||||
|
||||
def start(self):
|
||||
async 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 str(
|
||||
e
|
||||
): # FIXME: there is not good way to check bridge errors
|
||||
self.parser.error(
|
||||
_("unknown syntax requested ({syntax})").format(
|
||||
syntax=self.args.syntax
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
self.current_syntax = await self.get_current_syntax()
|
||||
|
||||
(
|
||||
self.pubsub_service,
|
||||
self.pubsub_node,
|
||||
self.pubsub_item,
|
||||
content_file_path,
|
||||
content_file_obj,
|
||||
mb_data,
|
||||
) = self.getItemPath()
|
||||
(self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path,
|
||||
content_file_obj, mb_data,) = await self.getItemPath()
|
||||
|
||||
self.edit(content_file_path, content_file_obj, mb_data=mb_data)
|
||||
await self.edit(content_file_path, content_file_obj, mb_data=mb_data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Preview(base.CommandBase, common.BaseEdit):
|
||||
|
|
@ -643,14 +646,14 @@ class Preview(base.CommandBase, common.BaseEdit):
|
|||
help=_("path to the content file"),
|
||||
)
|
||||
|
||||
def showPreview(self):
|
||||
async 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))
|
||||
url = "file:{}".format(self.urllib.parse.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))
|
||||
async def _launchPreviewExt(self, cmd_line, opt_name):
|
||||
url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path))
|
||||
args = common.parse_args(
|
||||
self.host, cmd_line, url=url, preview_file=self.preview_file_path
|
||||
)
|
||||
|
|
@ -662,34 +665,34 @@ class Preview(base.CommandBase, common.BaseEdit):
|
|||
self.host.quit(1)
|
||||
subprocess.Popen(args)
|
||||
|
||||
def openPreviewExt(self):
|
||||
self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
|
||||
async def openPreviewExt(self):
|
||||
await self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
|
||||
|
||||
def updatePreviewExt(self):
|
||||
self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
|
||||
async def updatePreviewExt(self):
|
||||
await self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
|
||||
|
||||
def updateContent(self):
|
||||
with open(self.content_file_path, "rb") as f:
|
||||
async def updateContent(self):
|
||||
with self.content_file_path.open("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 = await self.host.bridge.syntaxConvert(
|
||||
content, self.syntax, SYNTAX_XHTML, True, self.profile
|
||||
)
|
||||
|
||||
xhtml = (
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml">'
|
||||
'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
|
||||
"</head>"
|
||||
"<body>{}</body>"
|
||||
"</html>"
|
||||
).format(content)
|
||||
f'<html xmlns="http://www.w3.org/1999/xhtml">'
|
||||
f'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
|
||||
f'</head>'
|
||||
f'<body>{content}</body>'
|
||||
f'</html>'
|
||||
)
|
||||
|
||||
with open(self.preview_file_path, "wb") as f:
|
||||
f.write(xhtml.encode("utf-8"))
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
import webbrowser
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
|
|
@ -697,33 +700,24 @@ class Preview(base.CommandBase, common.BaseEdit):
|
|||
|
||||
if self.args.inotify != "false":
|
||||
try:
|
||||
import inotify.adapters
|
||||
import inotify.constants
|
||||
from inotify.calls import InotifyError
|
||||
import aionotify
|
||||
|
||||
except ImportError:
|
||||
if self.args.inotify == "auto":
|
||||
inotify = None
|
||||
aionotify = None
|
||||
self.disp(
|
||||
"inotify module not found, deactivating feature. You can install"
|
||||
" it with {install}".format(install=INOTIFY_INSTALL)
|
||||
f"aionotify module not found, deactivating feature. You can "
|
||||
f"install it with {AIONOTIFY_INSTALL}"
|
||||
)
|
||||
else:
|
||||
self.disp(
|
||||
"inotify not found, can't activate the feature! Please install "
|
||||
"it with {install}".format(install=INOTIFY_INSTALL),
|
||||
f"aioinotify not found, can't activate the feature! Please "
|
||||
f"install it with {AIONOTIFY_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(
|
||||
"Logger doesn't exists, inotify may have chanded", error=True
|
||||
)
|
||||
else:
|
||||
inotify = None
|
||||
aionotify = None
|
||||
|
||||
sat_conf = config.parseMainConf()
|
||||
SYNTAX_EXT.update(config.getConfig(sat_conf, "jp", CONF_SYNTAX_EXT, {}))
|
||||
|
|
@ -750,76 +744,89 @@ class Preview(base.CommandBase, common.BaseEdit):
|
|||
if self.args.file == "current":
|
||||
self.content_file_path = self.getCurrentFile(self.profile)
|
||||
else:
|
||||
self.content_file_path = os.path.abspath(self.args.file)
|
||||
try:
|
||||
self.content_file_path = Path(self.args.file).resolve(strict=True)
|
||||
except FileNotFoundError:
|
||||
self.disp(_(f'File "{self.args.file}" doesn\'t exist!'))
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
|
||||
self.syntax = guessSyntaxFromPath(self.host, sat_conf, self.content_file_path)
|
||||
self.syntax = await guessSyntaxFromPath(
|
||||
self.host, 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()
|
||||
await self.updateContent()
|
||||
|
||||
if inotify is None:
|
||||
# XXX: we don't delete file automatically because browser need it
|
||||
if aionotify is None:
|
||||
# XXX: we don't delete file automatically because browser needs it
|
||||
# (and webbrowser.open can return before it is read)
|
||||
self.disp(
|
||||
"temporary file created at {}\nthis file will NOT BE DELETED "
|
||||
"AUTOMATICALLY, please delete it yourself when you have finished".format(
|
||||
self.preview_file_path
|
||||
)
|
||||
f"temporary file created at {self.preview_file_path}\nthis file will NOT "
|
||||
f"BE DELETED AUTOMATICALLY, please delete it yourself when you have "
|
||||
f"finished"
|
||||
)
|
||||
open_cb()
|
||||
await open_cb()
|
||||
else:
|
||||
open_cb()
|
||||
i = inotify.adapters.Inotify(
|
||||
block_duration_s=60
|
||||
) # no need for 1 s duraction, inotify drive actions here
|
||||
await open_cb()
|
||||
watcher = aionotify.Watcher()
|
||||
watcher_kwargs = {
|
||||
# Watcher don't accept Path so we convert to string
|
||||
"path": str(self.content_file_path),
|
||||
"alias": 'content_file',
|
||||
"flags": aionotify.Flags.CLOSE_WRITE
|
||||
| aionotify.Flags.DELETE_SELF
|
||||
| aionotify.Flags.MOVE_SELF,
|
||||
}
|
||||
watcher.watch(**watcher_kwargs)
|
||||
|
||||
def add_watch():
|
||||
i.add_watch(
|
||||
self.content_file_path.encode('utf-8'),
|
||||
mask=inotify.constants.IN_CLOSE_WRITE
|
||||
| inotify.constants.IN_DELETE_SELF
|
||||
| inotify.constants.IN_MOVE_SELF,
|
||||
)
|
||||
|
||||
add_watch()
|
||||
loop = asyncio.get_event_loop()
|
||||
await watcher.setup(loop)
|
||||
|
||||
try:
|
||||
for event in i.event_gen():
|
||||
if event is not None:
|
||||
self.disp("Content updated", 1)
|
||||
if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]):
|
||||
while True:
|
||||
event = await watcher.get_event()
|
||||
self.disp("Content updated", 1)
|
||||
if event.flags & (aionotify.Flags.DELETE_SELF
|
||||
| aionotify.Flags.MOVE_SELF):
|
||||
self.disp(
|
||||
"DELETE/MOVE event catched, changing the watch",
|
||||
2,
|
||||
)
|
||||
try:
|
||||
watcher.unwatch('content_file')
|
||||
except IOError as e:
|
||||
self.disp(
|
||||
"{} event catched, changing the watch".format(
|
||||
", ".join(event[1])
|
||||
),
|
||||
f"Can't remove the watch: {e}",
|
||||
2,
|
||||
)
|
||||
i.remove_watch(self.content_file_path)
|
||||
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(
|
||||
"Can't catch inotify events, as the file been deleted?", error=True
|
||||
)
|
||||
watcher = aionotify.Watcher()
|
||||
watcher.watch(**watcher_kwargs)
|
||||
try:
|
||||
await watcher.setup(loop)
|
||||
except OSError:
|
||||
# if the new file is not here yet we can have an error
|
||||
# as a workaround, we do a little rest and try again
|
||||
await asyncio.sleep(1)
|
||||
await watcher.setup(loop)
|
||||
await self.updateContent()
|
||||
await update_cb()
|
||||
except FileNotFoundError:
|
||||
self.disp("The file seems to have been deleted.", error=True)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
finally:
|
||||
os.unlink(self.preview_file_path)
|
||||
try:
|
||||
i.remove_watch(self.content_file_path)
|
||||
except InotifyError:
|
||||
pass
|
||||
watcher.unwatch('content_file')
|
||||
except IOError as e:
|
||||
self.disp(
|
||||
f"Can't remove the watch: {e}",
|
||||
2,
|
||||
)
|
||||
|
||||
|
||||
class Import(base.CommandAnswering):
|
||||
class Import(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(Import, self).__init__(
|
||||
host,
|
||||
|
|
@ -828,7 +835,6 @@ class Import(base.CommandAnswering):
|
|||
use_progress=True,
|
||||
help=_("import an external blog"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -871,10 +877,10 @@ class Import(base.CommandAnswering):
|
|||
),
|
||||
)
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("Blog upload started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("Blog uploaded successfully"), 2)
|
||||
redirections = {
|
||||
k[len(URL_REDIRECT_PREFIX) :]: v
|
||||
|
|
@ -897,42 +903,35 @@ class Import(base.CommandAnswering):
|
|||
)
|
||||
self.disp(
|
||||
_(
|
||||
"\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)
|
||||
f"\nTo redirect old URLs to new ones, put the following lines in your"
|
||||
f" sat.conf file, in [libervia] section:\n\n{conf}"
|
||||
)
|
||||
)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
self.disp(_("Error while uploading blog: {}").format(error_msg), error=True)
|
||||
async def onProgressError(self, error_msg):
|
||||
self.disp(_(f"Error while uploading blog: {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):
|
||||
async 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(
|
||||
_(
|
||||
"{name} argument can't be used without location argument"
|
||||
).format(name=name)
|
||||
f"{name} argument can't be used without location argument"
|
||||
)
|
||||
)
|
||||
if self.args.importer is None:
|
||||
self.disp(
|
||||
"\n".join(
|
||||
[
|
||||
"{}: {}".format(name, desc)
|
||||
for name, desc in self.host.bridge.blogImportList()
|
||||
f"{name}: {desc}"
|
||||
for name, desc in await self.host.bridge.blogImportList()
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
short_desc, long_desc = self.host.bridge.blogImportDesc(
|
||||
short_desc, long_desc = await self.host.bridge.blogImportDesc(
|
||||
self.args.importer
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -942,13 +941,7 @@ class Import(base.CommandAnswering):
|
|||
self.disp(msg)
|
||||
self.host.quit(1)
|
||||
else:
|
||||
self.disp(
|
||||
"{name}: {short_desc}\n\n{long_desc}".format(
|
||||
name=self.args.importer,
|
||||
short_desc=short_desc,
|
||||
long_desc=long_desc,
|
||||
)
|
||||
)
|
||||
self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}")
|
||||
self.host.quit()
|
||||
else:
|
||||
# we have a location, an import is requested
|
||||
|
|
@ -967,20 +960,23 @@ class Import(base.CommandAnswering):
|
|||
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,
|
||||
)
|
||||
try:
|
||||
progress_id = await self.host.bridge.blogImport(
|
||||
self.args.importer,
|
||||
self.args.location,
|
||||
options,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
_(f"Error while trying to import a blog: {e}"),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
|
||||
await self.set_progress_id(progress_id)
|
||||
|
||||
class Blog(base.CommandBase):
|
||||
subcommands = (Set, Get, Edit, Preview, Import)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
from . import base
|
||||
from sat.core.i18n import _
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
|
||||
__commands__ = ["Bookmarks"]
|
||||
|
||||
|
|
@ -32,23 +33,25 @@ class BookmarksCommon(base.CommandBase):
|
|||
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)
|
||||
async def start(self):
|
||||
try:
|
||||
data = await self.host.bridge.bookmarksList(
|
||||
self.args.type, self.args.location, self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get bookmarks list: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
mess = []
|
||||
for location in STORAGE_LOCATIONS:
|
||||
if not data[location]:
|
||||
continue
|
||||
loc_mess = []
|
||||
loc_mess.append("%s:" % location)
|
||||
loc_mess.append(f"{location}:")
|
||||
book_mess = []
|
||||
for book_link, book_data in list(data[location].items()):
|
||||
name = book_data.get('name')
|
||||
|
|
@ -62,41 +65,56 @@ class BookmarksList(BookmarksCommon):
|
|||
mess.append('\n'.join(loc_mess))
|
||||
|
||||
print('\n\n'.join(mess))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
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', help=_('jid (for muc bookmark) or url of to remove'))
|
||||
self.parser.add_argument(
|
||||
'bookmark', help=_('jid (for muc bookmark) or url of to remove'))
|
||||
self.parser.add_argument(
|
||||
"-f", "--force", action="store_true",
|
||||
help=_("delete bookmark without confirmation"),)
|
||||
|
||||
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)
|
||||
async def start(self):
|
||||
if not self.args.force:
|
||||
await self.host.confirmOrQuit(_("Are you sure to delete this bookmark?"))
|
||||
|
||||
try:
|
||||
await self.host.bridge.bookmarksRemove(
|
||||
self.args.type, self.args.bookmark, self.args.location, self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(_(f"can't delete bookmark: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_('bookmark deleted'))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
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', help=_('jid (for muc bookmark) or url of to remove'))
|
||||
self.parser.add_argument(
|
||||
'bookmark', help=_('jid (for muc bookmark) or url of to remove'))
|
||||
self.parser.add_argument('-n', '--name', help=_("bookmark name"))
|
||||
muc_group = self.parser.add_argument_group(_('MUC specific options'))
|
||||
muc_group.add_argument('-N', '--nick', help=_('nickname'))
|
||||
muc_group.add_argument('-a', '--autojoin', action='store_true', help=_('join room on profile connection'))
|
||||
muc_group.add_argument(
|
||||
'-a', '--autojoin', action='store_true',
|
||||
help=_('join room on profile connection'))
|
||||
|
||||
def start(self):
|
||||
async 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(_("You can't use --autojoin or --nick with --type url"))
|
||||
self.host.quit(1)
|
||||
self.parser.error(_("You can't use --autojoin or --nick with --type url"))
|
||||
data = {}
|
||||
if self.args.autojoin:
|
||||
data['autojoin'] = 'true'
|
||||
|
|
@ -104,7 +122,16 @@ class BookmarksAdd(BookmarksCommon):
|
|||
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)
|
||||
try:
|
||||
await self.host.bridge.bookmarksAdd(
|
||||
self.args.type, self.args.bookmark, data, self.args.location,
|
||||
self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't add bookmark: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_('bookmark successfully added'))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Bookmarks(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class Method(base.CommandBase, BridgeCommon):
|
|||
def __init__(self, host):
|
||||
base.CommandBase.__init__(self, host, "method", help=_("call a bridge method"))
|
||||
BridgeCommon.__init__(self)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -58,34 +57,31 @@ class Method(base.CommandBase, BridgeCommon):
|
|||
"arg", nargs="*", help=_("argument of the method")
|
||||
)
|
||||
|
||||
def method_cb(self, ret=None):
|
||||
if ret is not None:
|
||||
self.disp(str(ret))
|
||||
self.host.quit()
|
||||
|
||||
def method_eb(self, failure):
|
||||
self.disp(
|
||||
_("Error while executing {}: {}".format(self.args.method, failure)),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
method = getattr(self.host.bridge, self.args.method)
|
||||
import inspect
|
||||
argspec = inspect.getargspec(method)
|
||||
|
||||
kwargs = {}
|
||||
if 'profile_key' in argspec.args:
|
||||
kwargs['profile_key'] = self.profile
|
||||
elif 'profile' in argspec.args:
|
||||
kwargs['profile'] = self.profile
|
||||
|
||||
args = self.evalArgs()
|
||||
|
||||
try:
|
||||
method(
|
||||
ret = await method(
|
||||
*args,
|
||||
profile=self.profile,
|
||||
callback=self.method_cb,
|
||||
errback=self.method_eb
|
||||
**kwargs,
|
||||
)
|
||||
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(_("bad arguments"))
|
||||
except Exception as e:
|
||||
self.disp(_(f"Error while executing {self.args.method}: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
else:
|
||||
if ret is not None:
|
||||
self.disp(str(ret))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Signal(base.CommandBase, BridgeCommon):
|
||||
|
|
@ -103,12 +99,18 @@ class Signal(base.CommandBase, BridgeCommon):
|
|||
"arg", nargs="*", help=_("argument of the signal")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async 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)
|
||||
try:
|
||||
await self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile)
|
||||
except Exception as e:
|
||||
self.disp(_(f"Can't send fake signal: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Bridge(base.CommandBase):
|
||||
|
|
@ -130,7 +132,6 @@ class Monitor(base.CommandBase):
|
|||
use_output=C.OUTPUT_XML,
|
||||
help=_("monitor XML stream"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -141,7 +142,7 @@ class Monitor(base.CommandBase):
|
|||
help=_("stream direction filter"),
|
||||
)
|
||||
|
||||
def printXML(self, direction, xml_data, profile):
|
||||
async def printXML(self, direction, xml_data, profile):
|
||||
if self.args.direction == "in" and direction != "IN":
|
||||
return
|
||||
if self.args.direction == "out" and direction != "OUT":
|
||||
|
|
@ -155,7 +156,7 @@ class Monitor(base.CommandBase):
|
|||
whiteping = False
|
||||
|
||||
if verbosity:
|
||||
profile_disp = " ({})".format(profile) if verbosity > 1 else ""
|
||||
profile_disp = f" ({profile})" if verbosity > 1 else ""
|
||||
if direction == "IN":
|
||||
self.disp(
|
||||
A.color(
|
||||
|
|
@ -172,7 +173,7 @@ class Monitor(base.CommandBase):
|
|||
self.disp("[WHITESPACE PING]")
|
||||
else:
|
||||
try:
|
||||
self.output(xml_data)
|
||||
await self.output(xml_data)
|
||||
except Exception:
|
||||
# initial stream is not valid XML,
|
||||
# in this case we print directly to data
|
||||
|
|
@ -182,7 +183,7 @@ class Monitor(base.CommandBase):
|
|||
self.disp(xml_data)
|
||||
self.disp("")
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
self.host.bridge.register_signal("xmlLog", self.printXML, "plugin")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
from sat_frontends.jp import base
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from sat.core.i18n import _
|
||||
from functools import partial
|
||||
from sat.tools.common import data_format
|
||||
from sat_frontends.jp import xmlui_manager
|
||||
|
||||
|
|
@ -37,33 +36,26 @@ class EncryptionAlgorithms(base.CommandBase):
|
|||
extra_outputs=extra_outputs,
|
||||
use_profile=False,
|
||||
help=_("show available encryption algorithms"))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def encryptionPluginsGetCb(self, plugins):
|
||||
self.output(plugins)
|
||||
self.host.quit()
|
||||
|
||||
def default_output(self, plugins):
|
||||
if not plugins:
|
||||
self.disp(_("No encryption plugin registered!"))
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
else:
|
||||
self.disp(_("Following encryption algorithms are available: {algos}").format(
|
||||
algos=', '.join([p['name'] for p in plugins])))
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.encryptionPluginsGet(
|
||||
callback=self.encryptionPluginsGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't retrieve plugins: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
plugins = await self.host.bridge.encryptionPluginsGet()
|
||||
except Exception as e:
|
||||
self.disp(f"can't retrieve plugins: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(plugins)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class EncryptionGet(base.CommandBase):
|
||||
|
|
@ -73,7 +65,6 @@ class EncryptionGet(base.CommandBase):
|
|||
host, "get",
|
||||
use_output=C.OUTPUT_DICT,
|
||||
help=_("get encryption session data"))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -81,28 +72,23 @@ class EncryptionGet(base.CommandBase):
|
|||
help=_("jid of the entity to check")
|
||||
)
|
||||
|
||||
def messageEncryptionGetCb(self, serialised):
|
||||
async def start(self):
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
try:
|
||||
serialised = await self.host.bridge.messageEncryptionGet(jid, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get session: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
session_data = data_format.deserialise(serialised)
|
||||
if session_data is None:
|
||||
self.disp(
|
||||
"No encryption session found, the messages are sent in plain text.")
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
self.output(session_data)
|
||||
await self.output(session_data)
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
self.host.bridge.messageEncryptionGet(
|
||||
jid, self.profile,
|
||||
callback=self.messageEncryptionGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't get session: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EncryptionStart(base.CommandBase):
|
||||
|
||||
|
|
@ -110,7 +96,6 @@ class EncryptionStart(base.CommandBase):
|
|||
super(EncryptionStart, self).__init__(
|
||||
host, "start",
|
||||
help=_("start encrypted session with an entity"))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -128,30 +113,30 @@ class EncryptionStart(base.CommandBase):
|
|||
help=_("jid of the entity to stop encrypted session with")
|
||||
)
|
||||
|
||||
def encryptionNamespaceGetCb(self, namespace):
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
self.host.bridge.messageEncryptionStart(
|
||||
jid, namespace, not self.args.encrypt_noreplace,
|
||||
self.profile,
|
||||
callback=self.host.quit,
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't start encryption session: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.name is not None:
|
||||
self.host.bridge.encryptionNamespaceGet(self.args.name,
|
||||
callback=self.encryptionNamespaceGetCb,
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't get encryption namespace: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
try:
|
||||
namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get encryption namespace: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
elif self.args.namespace is not None:
|
||||
self.encryptionNamespaceGetCb(self.args.namespace)
|
||||
namespace = self.args.namespace
|
||||
else:
|
||||
self.encryptionNamespaceGetCb("")
|
||||
namespace = ""
|
||||
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
|
||||
try:
|
||||
await self.host.bridge.messageEncryptionStart(
|
||||
jid, namespace, not self.args.encrypt_noreplace,
|
||||
self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get encryption namespace: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class EncryptionStop(base.CommandBase):
|
||||
|
|
@ -160,7 +145,6 @@ class EncryptionStop(base.CommandBase):
|
|||
super(EncryptionStop, self).__init__(
|
||||
host, "stop",
|
||||
help=_("stop encrypted session with an entity"))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -168,18 +152,16 @@ class EncryptionStop(base.CommandBase):
|
|||
help=_("jid of the entity to stop encrypted session with")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
async def start(self):
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
self.host.bridge.messageEncryptionStop(
|
||||
jid, self.profile,
|
||||
callback=self.host.quit,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't end encrypted session: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.messageEncryptionStop(jid, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't end encrypted session: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class TrustUI(base.CommandBase):
|
||||
|
|
@ -188,7 +170,6 @@ class TrustUI(base.CommandBase):
|
|||
super(TrustUI, self).__init__(
|
||||
host, "ui",
|
||||
help=_("get UI to manage trust"))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -202,37 +183,33 @@ class TrustUI(base.CommandBase):
|
|||
"-N", "--namespace",
|
||||
help=_("algorithm namespace (DEFAULT: current algorithm)"))
|
||||
|
||||
def encryptionTrustUIGetCb(self, xmlui_raw):
|
||||
xmlui = xmlui_manager.create(self.host, xmlui_raw)
|
||||
xmlui.show()
|
||||
xmlui.submitForm()
|
||||
|
||||
def encryptionNamespaceGetCb(self, namespace):
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
self.host.bridge.encryptionTrustUIGet(
|
||||
jid, namespace, self.profile,
|
||||
callback=self.encryptionTrustUIGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't end encrypted session: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.name is not None:
|
||||
self.host.bridge.encryptionNamespaceGet(self.args.name,
|
||||
callback=self.encryptionNamespaceGetCb,
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't get encryption namespace: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
try:
|
||||
namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get encryption namespace: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
elif self.args.namespace is not None:
|
||||
self.encryptionNamespaceGetCb(self.args.namespace)
|
||||
namespace = self.args.namespace
|
||||
else:
|
||||
self.encryptionNamespaceGetCb("")
|
||||
namespace = ""
|
||||
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
|
||||
try:
|
||||
xmlui_raw = await self.host.bridge.encryptionTrustUIGet(
|
||||
jid, namespace, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get encryption session trust UI: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
xmlui = xmlui_manager.create(self.host, xmlui_raw)
|
||||
await xmlui.show()
|
||||
if xmlui.type != C.XMLUI_DIALOG:
|
||||
await xmlui.submitForm()
|
||||
self.host.quit()
|
||||
|
||||
class EncryptionTrust(base.CommandBase):
|
||||
subcommands = (TrustUI,)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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
|
||||
|
|
@ -47,30 +46,26 @@ class Get(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("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=_("can't get event data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
event_tuple = await self.host.bridge.eventGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.item,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get event data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
event_date, event_data = event_tuple
|
||||
event_data["date"] = event_date
|
||||
await self.output(event_data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class EventBase(object):
|
||||
|
|
@ -126,29 +121,25 @@ class Create(EventBase, base.CommandBase):
|
|||
help=_("create or replace event"),
|
||||
)
|
||||
EventBase.__init__(self)
|
||||
self.need_loop = True
|
||||
|
||||
def eventCreateCb(self, node):
|
||||
self.disp(_("Event created successfuly on node {node}").format(node=node))
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
async 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=_("can't create event: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
node = await self.host.bridge.eventCreate(
|
||||
date,
|
||||
fields,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.id,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create event: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_(f"Event created successfuly on node {node}"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Modify(EventBase, base.CommandBase):
|
||||
|
|
@ -161,25 +152,24 @@ class Modify(EventBase, base.CommandBase):
|
|||
help=_("modify an existing event"),
|
||||
)
|
||||
EventBase.__init__(self)
|
||||
self.need_loop = True
|
||||
|
||||
def start(self):
|
||||
async 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=_("can't update event data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.host.bridge.eventModify(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.id,
|
||||
date,
|
||||
fields,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't update event data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class InviteeGet(base.CommandBase):
|
||||
|
|
@ -190,32 +180,31 @@ class InviteeGet(base.CommandBase):
|
|||
"get",
|
||||
use_output=C.OUTPUT_DICT,
|
||||
use_pubsub=True,
|
||||
pubsub_flags={C.NODE, C.ITEM, C.SINGLE_ITEM},
|
||||
pubsub_flags={C.NODE},
|
||||
use_verbose=True,
|
||||
help=_("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=_("can't get event data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
self.parser.add_argument(
|
||||
"-j", "--jid", default="", help=_("bare jid of the invitee")
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
event_data = await self.host.bridge.eventInviteeGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.jid,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get event data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(event_data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class InviteeSet(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
|
|
@ -227,7 +216,6 @@ class InviteeSet(base.CommandBase):
|
|||
pubsub_flags={C.NODE},
|
||||
help=_("set event attendance"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -240,20 +228,20 @@ class InviteeSet(base.CommandBase):
|
|||
help=_("configuration field to set"),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async 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=_("can't set event data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.host.bridge.eventInviteeSet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
fields,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set event data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class InviteesList(base.CommandBase):
|
||||
|
|
@ -270,7 +258,6 @@ class InviteesList(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("get event attendance"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -399,72 +386,68 @@ class InviteesList(base.CommandBase):
|
|||
)
|
||||
)
|
||||
|
||||
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.items():
|
||||
id_data = self.host.bridge.identityGet(jid_, self.profile)
|
||||
data["nick"] = id_data.get("nick", "")
|
||||
|
||||
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=_("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.items()
|
||||
if affiliation in ("publisher",)
|
||||
}
|
||||
self.getList(prefilled)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.no_rsvp and not self.args.missing:
|
||||
self.parser.error(_("you need to use --missing if you use --no-rsvp"))
|
||||
if self.args.missing:
|
||||
self.host.bridge.psNodeAffiliationsGet(
|
||||
if not self.args.missing:
|
||||
prefilled = {}
|
||||
else:
|
||||
# we get prefilled data with all people
|
||||
try:
|
||||
affiliations = await self.host.bridge.psNodeAffiliationsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get node affiliations: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
# we fill all affiliations with empty data, answered one will be filled
|
||||
# below. We only consider people with "publisher" affiliation as invited,
|
||||
# creators are not, and members can just observe
|
||||
prefilled = {
|
||||
jid_: {}
|
||||
for jid_, affiliation in affiliations.items()
|
||||
if affiliation in ("publisher",)
|
||||
}
|
||||
|
||||
try:
|
||||
event_data = await self.host.bridge.eventInviteesList(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
callback=self.psNodeAffiliationsGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't get event data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get event data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
# we fill nicknames and keep only requested people
|
||||
|
||||
if self.args.no_rsvp:
|
||||
for jid_ in event_data:
|
||||
# if there is a jid in event_data it must be there in prefilled too
|
||||
# otherwie somebody is not on the invitees list
|
||||
try:
|
||||
del prefilled[jid_]
|
||||
except KeyError:
|
||||
self.disp(A.color(
|
||||
C.A_WARNING,
|
||||
f"We got a RSVP from somebody who was not in invitees "
|
||||
f"list: {jid_}"
|
||||
),
|
||||
error=True)
|
||||
else:
|
||||
self.getList()
|
||||
# we replace empty dicts for existing people with R.S.V.P. data
|
||||
prefilled.update(event_data)
|
||||
|
||||
# we get nicknames for everybody, make it easier for organisers
|
||||
for jid_, data in prefilled.items():
|
||||
id_data = await self.host.bridge.identityGet(jid_, self.profile)
|
||||
data["nick"] = id_data.get("nick", "")
|
||||
|
||||
await self.output(prefilled)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class InviteeInvite(base.CommandBase):
|
||||
|
|
@ -477,7 +460,6 @@ class InviteeInvite(base.CommandBase):
|
|||
pubsub_flags={C.NODE, C.SINGLE_ITEM},
|
||||
help=_("invite someone to the event through email"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -524,30 +506,30 @@ class InviteeInvite(base.CommandBase):
|
|||
help="body of the invitation email (default: generic body)",
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
email = self.args.email[0] if self.args.email else None
|
||||
emails_extra = self.args.email[1:]
|
||||
|
||||
self.host.bridge.eventInviteByEmail(
|
||||
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=_("can't create invitation: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.eventInviteByEmail(
|
||||
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,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create invitation: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Invitee(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ 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"]
|
||||
|
|
@ -46,7 +45,6 @@ class Send(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("send a file to a contact"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -75,23 +73,19 @@ class Send(base.CommandBase):
|
|||
help=("name to use (DEFAULT: use source file name)"),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Send files to jabber contact"""
|
||||
self.send_files()
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("File copy started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("File sent successfully"), 2)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
async def onProgressError(self, error_msg):
|
||||
if error_msg == C.PROGRESS_ERROR_DECLINED:
|
||||
self.disp(_("The file has been refused by your contact"))
|
||||
else:
|
||||
self.disp(_("Error while sending file: {}").format(error_msg), error=True)
|
||||
|
||||
def gotId(self, data, file_):
|
||||
async def gotId(self, data, file_):
|
||||
"""Called when a progress id has been received
|
||||
|
||||
@param pid(unicode): progress id
|
||||
|
|
@ -100,7 +94,7 @@ class Send(base.CommandBase):
|
|||
# FIXME: this show progress only for last progress_id
|
||||
self.disp(_("File request sent to {jid}".format(jid=self.full_dest_jid)), 1)
|
||||
try:
|
||||
self.progress_id = data["progress"]
|
||||
await self.set_progress_id(data["progress"])
|
||||
except KeyError:
|
||||
# TODO: if 'xmlui' key is present, manage xmlui message display
|
||||
self.disp(
|
||||
|
|
@ -108,27 +102,18 @@ class Send(base.CommandBase):
|
|||
)
|
||||
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):
|
||||
async def start(self):
|
||||
for file_ in self.args.files:
|
||||
if not os.path.exists(file_):
|
||||
self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
|
||||
self.host.quit(1)
|
||||
self.disp(_(f"file {file_!r} doesn't exist!"), error=True)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
if not self.args.bz2 and os.path.isdir(file_):
|
||||
self.disp(
|
||||
_(
|
||||
"[{}] is a dir ! Please send files inside or use compression"
|
||||
).format(file_)
|
||||
_(f"{file_!r} is a dir! Please send files inside or use compression")
|
||||
)
|
||||
self.host.quit(1)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
|
||||
self.full_dest_jid = self.host.get_full_jid(self.args.jid)
|
||||
self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
|
||||
extra = {}
|
||||
if self.args.path:
|
||||
extra["path"] = self.args.path
|
||||
|
|
@ -152,29 +137,37 @@ class Send(base.CommandBase):
|
|||
bz2.close()
|
||||
self.disp(_("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,
|
||||
)
|
||||
try:
|
||||
send_data = await self.host.bridge.fileSend(
|
||||
self.full_dest_jid,
|
||||
buf.name,
|
||||
self.args.name or archive_name,
|
||||
"",
|
||||
extra,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't send file: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.gotId(send_data, file_)
|
||||
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,
|
||||
)
|
||||
try:
|
||||
send_data = await self.host.bridge.fileSend(
|
||||
self.full_dest_jid,
|
||||
path,
|
||||
self.args.name,
|
||||
"",
|
||||
extra,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't send file {file_!r}: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.gotId(send_data, file_)
|
||||
|
||||
|
||||
class Request(base.CommandBase):
|
||||
|
|
@ -186,7 +179,6 @@ class Request(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("request a file from a contact"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
|
|
@ -200,7 +192,8 @@ class Request(base.CommandBase):
|
|||
"-D",
|
||||
"--dest",
|
||||
help=_(
|
||||
"destination path where the file will be saved (default: [current_dir]/[name|hash])"
|
||||
"destination path where the file will be saved (default: "
|
||||
"[current_dir]/[name|hash])"
|
||||
),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
|
|
@ -238,36 +231,21 @@ class Request(base.CommandBase):
|
|||
help=_("overwrite existing file without confirmation"),
|
||||
)
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("File copy started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("File received successfully"), 2)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
async def onProgressError(self, error_msg):
|
||||
if error_msg == C.PROGRESS_ERROR_DECLINED:
|
||||
self.disp(_("The file request has been refused"))
|
||||
else:
|
||||
self.disp(_("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):
|
||||
async def start(self):
|
||||
if not self.args.name and not self.args.hash:
|
||||
self.parser.error(_("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):
|
||||
|
|
@ -276,35 +254,30 @@ class Request(base.CommandBase):
|
|||
path = os.path.abspath(self.filename)
|
||||
|
||||
if os.path.exists(path) and not self.args.force:
|
||||
message = _("File {path} already exists! Do you want to overwrite?").format(
|
||||
path=path
|
||||
)
|
||||
confirm = input("{} (y/N) ".format(message).encode("utf-8"))
|
||||
if confirm not in ("y", "Y"):
|
||||
self.disp(_("file request cancelled"))
|
||||
self.host.quit(2)
|
||||
message = _(f"File {path} already exists! Do you want to overwrite?")
|
||||
await self.host.confirmOrQuit(message, _("file request cancelled"))
|
||||
|
||||
self.full_dest_jid = self.host.get_full_jid(self.args.jid)
|
||||
self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
|
||||
extra = {}
|
||||
if self.args.path:
|
||||
extra["path"] = self.args.path
|
||||
if self.args.namespace:
|
||||
extra["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 "",
|
||||
extra,
|
||||
self.profile,
|
||||
callback=self.gotId,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't request file: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
progress_id = await self.host.bridge.fileJingleRequest(
|
||||
self.full_dest_jid,
|
||||
path,
|
||||
self.args.name,
|
||||
self.args.hash,
|
||||
self.args.hash_algo if self.args.hash else "",
|
||||
extra,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(msg=_(f"can't request file: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.set_progress_id(progress_id)
|
||||
|
||||
|
||||
class Receive(base.CommandAnswering):
|
||||
|
|
@ -322,89 +295,6 @@ class Receive(base.CommandAnswering):
|
|||
C.META_TYPE_OVERWRITE: self.onOverwriteAction,
|
||||
}
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
self.disp(_("File copy started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
self.disp(_("File received successfully"), 2)
|
||||
if metadata.get("hash_verified", False):
|
||||
try:
|
||||
self.disp(
|
||||
_("hash checked: {algo}:{checksum}").format(
|
||||
algo=metadata["hash_algo"], checksum=metadata["hash"]
|
||||
),
|
||||
1,
|
||||
)
|
||||
except KeyError:
|
||||
self.disp(_("hash is checked but hash value is missing", 1), error=True)
|
||||
else:
|
||||
self.disp(_("hash can't be verified"), 1)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
self.disp(_("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(_("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(_("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(_("Ignoring action without from_jid data"), 1)
|
||||
return
|
||||
try:
|
||||
progress_id = action_data["meta_progress_id"]
|
||||
except KeyError:
|
||||
self.disp(_("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(_("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(_("ignoring action without progress id"), 1)
|
||||
return
|
||||
self.disp(_("Overwriting needed"), 1)
|
||||
|
||||
if progress_id == self.progress_id:
|
||||
if self.args.force:
|
||||
self.disp(_("Overwrite accepted"), 2)
|
||||
else:
|
||||
self.disp(_("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",
|
||||
|
|
@ -432,15 +322,95 @@ class Receive(base.CommandAnswering):
|
|||
help=_("destination path (default: working directory)"),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("File copy started"), 2)
|
||||
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("File received successfully"), 2)
|
||||
if metadata.get("hash_verified", False):
|
||||
try:
|
||||
self.disp(_(
|
||||
f"hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1)
|
||||
except KeyError:
|
||||
self.disp(_("hash is checked but hash value is missing", 1), error=True)
|
||||
else:
|
||||
self.disp(_("hash can't be verified"), 1)
|
||||
|
||||
async def onProgressError(self, e):
|
||||
self.disp(_(f"Error while receiving file: {e}"), 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(_("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(_("Invalid XMLUI received"), error=True)
|
||||
return xmlui_id
|
||||
|
||||
async 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(_("Ignoring action without from_jid data"), 1)
|
||||
return
|
||||
try:
|
||||
progress_id = action_data["meta_progress_id"]
|
||||
except KeyError:
|
||||
self.disp(_("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(_("File refused because overwrite is needed"), error=True)
|
||||
await self.host.bridge.launchAction(
|
||||
xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile
|
||||
)
|
||||
return self.host.quitFromSignal(2)
|
||||
await self.set_progress_id(progress_id)
|
||||
xmlui_data = {"path": self.path}
|
||||
await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
|
||||
|
||||
async 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(_("ignoring action without progress id"), 1)
|
||||
return
|
||||
self.disp(_("Overwriting needed"), 1)
|
||||
|
||||
if progress_id == self.progress_id:
|
||||
if self.args.force:
|
||||
self.disp(_("Overwrite accepted"), 2)
|
||||
else:
|
||||
self.disp(_("Refused to overwrite"), 2)
|
||||
self._overwrite_refused = True
|
||||
|
||||
xmlui_data = {"answer": C.boolConst(self.args.force)}
|
||||
await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
|
||||
|
||||
async 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(_("Given path is not a directory !", error=True))
|
||||
self.host.quit(2)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
if self.args.multiple:
|
||||
self.host.quit_on_progress_end = False
|
||||
self.disp(_("waiting for incoming file request"), 2)
|
||||
await self.start_answering()
|
||||
|
||||
|
||||
class Upload(base.CommandBase):
|
||||
|
|
@ -448,7 +418,6 @@ class Upload(base.CommandBase):
|
|||
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"))
|
||||
|
|
@ -463,10 +432,10 @@ class Upload(base.CommandBase):
|
|||
help=_("ignore invalide TLS certificate"),
|
||||
)
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("File upload started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("File uploaded successfully"), 2)
|
||||
try:
|
||||
url = metadata["url"]
|
||||
|
|
@ -477,55 +446,54 @@ class Upload(base.CommandBase):
|
|||
# XXX: url is display alone on a line to make parsing easier
|
||||
self.disp(url)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
async def onProgressError(self, error_msg):
|
||||
self.disp(_("Error while uploading file: {}").format(error_msg), error=True)
|
||||
|
||||
def gotId(self, data, file_):
|
||||
async 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"]
|
||||
await self.set_progress_id(data["progress"])
|
||||
except KeyError:
|
||||
# TODO: if 'xmlui' key is present, manage xmlui message display
|
||||
self.disp(_("Can't upload file"), error=True)
|
||||
self.host.quit(2)
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
|
||||
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):
|
||||
async def start(self):
|
||||
file_ = self.args.file
|
||||
if not os.path.exists(file_):
|
||||
self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
|
||||
self.host.quit(1)
|
||||
self.disp(_(f"file {file_!r} doesn't exist !"), error=True)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
if os.path.isdir(file_):
|
||||
self.disp(_("[{}] is a dir! Can't upload a dir").format(file_))
|
||||
self.host.quit(1)
|
||||
self.disp(_(f"{file_!r} is a dir! Can't upload a dir"))
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
|
||||
if self.args.jid is None:
|
||||
self.full_dest_jid = None
|
||||
else:
|
||||
self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
|
||||
|
||||
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,
|
||||
)
|
||||
try:
|
||||
upload_data = await self.host.bridge.fileUpload(
|
||||
path,
|
||||
"",
|
||||
self.full_dest_jid,
|
||||
options,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't while trying to upload a file: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.gotId(upload_data, file_)
|
||||
|
||||
|
||||
class ShareList(base.CommandBase):
|
||||
|
|
@ -539,7 +507,6 @@ class ShareList(base.CommandBase):
|
|||
help=_("retrieve files shared by an entity"),
|
||||
use_verbose=True,
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -555,12 +522,6 @@ class ShareList(base.CommandBase):
|
|||
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["name"]
|
||||
yield file_data.get("size", "")
|
||||
yield file_data.get("hash", "")
|
||||
|
||||
def _name_filter(self, name, row):
|
||||
if row.type == C.FILE_TYPE_DIRECTORY:
|
||||
return A.color(C.A_DIRECTORY, name)
|
||||
|
|
@ -588,38 +549,37 @@ class ShareList(base.CommandBase):
|
|||
files_data.sort(key=lambda d: d["name"].lower())
|
||||
show_header = False
|
||||
if self.verbosity == 0:
|
||||
headers = ("name", "type")
|
||||
keys = headers = ("name", "type")
|
||||
elif self.verbosity == 1:
|
||||
headers = ("name", "type", "size")
|
||||
keys = headers = ("name", "type", "size")
|
||||
elif self.verbosity > 1:
|
||||
show_header = True
|
||||
keys = ("name", "type", "size", "file_hash")
|
||||
headers = ("name", "type", "size", "hash")
|
||||
table = common.Table.fromDict(
|
||||
self.host,
|
||||
files_data,
|
||||
headers,
|
||||
keys=keys,
|
||||
headers=headers,
|
||||
filters={"name": self._name_filter, "size": self._size_filter},
|
||||
defaults={"size": "", "hash": ""},
|
||||
defaults={"size": "", "file_hash": ""},
|
||||
)
|
||||
table.display_blank(show_header=show_header, hide_cols=["type"])
|
||||
|
||||
def _FISListCb(self, files_data):
|
||||
self.output(files_data)
|
||||
self.host.quit()
|
||||
async def start(self):
|
||||
try:
|
||||
files_data = await self.host.bridge.FISList(
|
||||
self.args.jid,
|
||||
self.args.path,
|
||||
{},
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't retrieve shared files: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.FISList(
|
||||
self.args.jid,
|
||||
self.args.path,
|
||||
{},
|
||||
self.profile,
|
||||
callback=self._FISListCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't retrieve shared files: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
await self.output(files_data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class SharePath(base.CommandBase):
|
||||
|
|
@ -627,7 +587,6 @@ class SharePath(base.CommandBase):
|
|||
super(SharePath, self).__init__(
|
||||
host, "path", help=_("share a file or directory"), use_verbose=True
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -640,6 +599,7 @@ class SharePath(base.CommandBase):
|
|||
perm_group.add_argument(
|
||||
"-j",
|
||||
"--jid",
|
||||
metavar="JID",
|
||||
action="append",
|
||||
dest="jids",
|
||||
default=[],
|
||||
|
|
@ -649,7 +609,8 @@ class SharePath(base.CommandBase):
|
|||
"--public",
|
||||
action="store_true",
|
||||
help=_(
|
||||
"share publicly the file(s) (/!\\ *everybody* will be able to access them)"
|
||||
r"share publicly the file(s) (/!\ *everybody* will be able to access "
|
||||
r"them)"
|
||||
),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
|
|
@ -657,13 +618,7 @@ class SharePath(base.CommandBase):
|
|||
help=_("path to a file or directory to share"),
|
||||
)
|
||||
|
||||
def _FISSharePathCb(self, name):
|
||||
self.disp(
|
||||
_('{path} shared under the name "{name}"').format(path=self.path, name=name)
|
||||
)
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
self.path = os.path.abspath(self.args.path)
|
||||
if self.args.public:
|
||||
access = {"read": {"type": "public"}}
|
||||
|
|
@ -673,18 +628,19 @@ class SharePath(base.CommandBase):
|
|||
access = {"read": {"type": "whitelist", "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=_("can't share path: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
name = await self.host.bridge.FISSharePath(
|
||||
self.args.name,
|
||||
self.path,
|
||||
json.dumps(access, ensure_ascii=False),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't share path: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_(f'{self.path} shared under the name "{name}"'))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class ShareInvite(base.CommandBase):
|
||||
|
|
@ -692,7 +648,6 @@ class ShareInvite(base.CommandBase):
|
|||
super(ShareInvite, self).__init__(
|
||||
host, "invite", help=_("send invitation for a shared repository")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -733,13 +688,7 @@ class ShareInvite(base.CommandBase):
|
|||
help=_("jid of the person to invite"),
|
||||
)
|
||||
|
||||
def _FISInviteCb(self):
|
||||
self.disp(
|
||||
_('invitation sent to {entity}').format(entity=self.args.jid)
|
||||
)
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
self.path = os.path.normpath(self.args.path) if self.args.path else ""
|
||||
extra = {}
|
||||
if self.args.thumbnail is not None:
|
||||
|
|
@ -747,22 +696,25 @@ class ShareInvite(base.CommandBase):
|
|||
self.parser.error(_("only http(s) links are allowed with --thumbnail"))
|
||||
else:
|
||||
extra['thumb_url'] = self.args.thumbnail
|
||||
self.host.bridge.FISInvite(
|
||||
self.args.jid,
|
||||
self.args.service,
|
||||
self.args.type,
|
||||
self.args.namespace,
|
||||
self.path,
|
||||
self.args.name,
|
||||
data_format.serialise(extra),
|
||||
self.profile,
|
||||
callback=self._FISInviteCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't send invitation: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.FISInvite(
|
||||
self.args.jid,
|
||||
self.args.service,
|
||||
self.args.type,
|
||||
self.args.namespace,
|
||||
self.path,
|
||||
self.args.name,
|
||||
data_format.serialise(extra),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't send invitation: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(
|
||||
_(f'invitation sent to {self.args.jid}')
|
||||
)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Share(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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 codecs
|
||||
import json
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ class Edit(base.CommandBase, common.BaseEdit):
|
|||
help=_("edit forums"),
|
||||
)
|
||||
common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -60,26 +58,37 @@ class Edit(base.CommandBase, common.BaseEdit):
|
|||
"""return suffix used for content file"""
|
||||
return "json"
|
||||
|
||||
def forumsSetCb(self):
|
||||
self.disp(_("forums have been edited"), 1)
|
||||
self.host.quit()
|
||||
async def publish(self, forums_raw):
|
||||
try:
|
||||
await self.host.bridge.forumsSet(
|
||||
forums_raw,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.key,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set forums: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("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=_("can't set forums: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
forums_json = await self.host.bridge.forumsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.key,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
if e.classname == "NotFound":
|
||||
forums_json = ""
|
||||
else:
|
||||
self.disp(f"can't get node configuration: {e}", error=True)
|
||||
self.host.quit(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:
|
||||
|
|
@ -89,28 +98,7 @@ class Edit(base.CommandBase, common.BaseEdit):
|
|||
f = codecs.getwriter("utf-8")(content_file_obj)
|
||||
json.dump(forums, f, 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 == "item-not-found":
|
||||
self.forumsGetCb("")
|
||||
else:
|
||||
self.errback(
|
||||
failure_,
|
||||
msg=_("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,
|
||||
)
|
||||
await self.runEditor("forums_editor_args", content_file_path, content_file_obj)
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
|
|
@ -126,7 +114,6 @@ class Get(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("get forums structure"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -165,27 +152,24 @@ class Get(base.CommandBase):
|
|||
A.color(level * 4 * " ", head_color, key, A.RESET, ": ", value)
|
||||
)
|
||||
|
||||
def forumsGetCb(self, forums_raw):
|
||||
if not forums_raw:
|
||||
self.disp(_("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=_("can't get forums: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
forums_raw = await self.host.bridge.forumsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.key,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get forums: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
if not forums_raw:
|
||||
self.disp(_("no schema found"), 1)
|
||||
self.host.quit(1)
|
||||
forums = json.loads(forums_raw)
|
||||
await self.output(forums)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Forums(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -21,12 +21,9 @@
|
|||
from . 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):
|
||||
|
|
@ -38,29 +35,25 @@ class Get(base.CommandBase):
|
|||
use_verbose=True,
|
||||
help=_("get identity data"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
"jid", help=_("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=_("can't get identity data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
jid_ = (await self.host.check_jids([self.args.jid]))[0]
|
||||
try:
|
||||
data = await self.host.bridge.identityGet(
|
||||
jid_,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get identity data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Set(base.CommandBase):
|
||||
|
|
@ -78,20 +71,19 @@ class Set(base.CommandBase):
|
|||
required=True,
|
||||
help=_("identity field(s) to set"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
fields = dict(self.args.fields)
|
||||
self.host.bridge.identitySet(
|
||||
fields,
|
||||
self.profile,
|
||||
callback=self.host.quit,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't set identity data data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.host.bridge.identitySet(
|
||||
fields,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't set identity data: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Identity(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -31,53 +31,19 @@ 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
|
||||
super(Disco, self).__init__(
|
||||
host, 'disco', use_output='complex', extra_outputs=extra_outputs,
|
||||
help=_('service discovery'))
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument("jid", help=_("entity to discover"))
|
||||
self.parser.add_argument("-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both', help=_("type of data to discover"))
|
||||
self.parser.add_argument(
|
||||
"-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both',
|
||||
help=_("type of data to discover"))
|
||||
self.parser.add_argument("-n", "--node", default='', help=_("node to use"))
|
||||
self.parser.add_argument("-C", "--no-cache", dest='use_cache', action="store_false", help=_("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({
|
||||
'features': features,
|
||||
'identities': identities,
|
||||
'extensions': extensions})
|
||||
|
||||
if self.get_items:
|
||||
items.sort(key=lambda item: item[2])
|
||||
data['items'] = items
|
||||
|
||||
self.output(data)
|
||||
self.host.quit()
|
||||
self.parser.add_argument(
|
||||
"-C", "--no-cache", dest='use_cache', action="store_false",
|
||||
help=_("ignore cache"))
|
||||
|
||||
def default_output(self, data):
|
||||
features = data.get('features', [])
|
||||
|
|
@ -115,15 +81,18 @@ class Disco(base.CommandBase):
|
|||
for value in values:
|
||||
field_lines.append(A.color('\t - ', A.BOLD, value))
|
||||
fields.append('\n'.join(field_lines))
|
||||
extensions_tpl.append('{type_}\n{fields}'.format(type_=type_,
|
||||
fields='\n\n'.join(fields)))
|
||||
extensions_tpl.append('{type_}\n{fields}'.format(
|
||||
type_=type_,
|
||||
fields='\n\n'.join(fields)))
|
||||
|
||||
items_table = common.Table(self.host,
|
||||
items,
|
||||
headers=(_('entity'),
|
||||
_('node'),
|
||||
_('name')),
|
||||
use_buffer=True)
|
||||
items_table = common.Table(
|
||||
self.host,
|
||||
items,
|
||||
headers=(
|
||||
_('entity'),
|
||||
_('node'),
|
||||
_('name')),
|
||||
use_buffer=True)
|
||||
|
||||
template = []
|
||||
if features:
|
||||
|
|
@ -135,76 +104,131 @@ class Disco(base.CommandBase):
|
|||
if items:
|
||||
template.append(A.color(C.A_HEADER, _("Items")) + "\n\n{items}")
|
||||
|
||||
print("\n\n".join(template).format(features = '\n'.join(features),
|
||||
identities = identities_table.display().string,
|
||||
extensions = '\n'.join(extensions_tpl),
|
||||
items = items_table.display().string,
|
||||
))
|
||||
print("\n\n".join(template).format(
|
||||
features = '\n'.join(features),
|
||||
identities = identities_table.display().string,
|
||||
extensions = '\n'.join(extensions_tpl),
|
||||
items = items_table.display().string,
|
||||
))
|
||||
|
||||
async def start(self):
|
||||
infos_requested = self.args.type in ('infos', 'both')
|
||||
items_requested = self.args.type in ('items', 'both')
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
|
||||
# infos
|
||||
if not infos_requested:
|
||||
infos = None
|
||||
else:
|
||||
try:
|
||||
infos = await self.host.bridge.discoInfos(
|
||||
jid,
|
||||
node=self.args.node,
|
||||
use_cache=self.args.use_cache,
|
||||
profile_key=self.host.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(_(f"error while doing discovery: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
# items
|
||||
if not items_requested:
|
||||
items = None
|
||||
else:
|
||||
try:
|
||||
items = await self.host.bridge.discoItems(
|
||||
jid,
|
||||
node=self.args.node,
|
||||
use_cache=self.args.use_cache,
|
||||
profile_key=self.host.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(_(f"error while doing discovery: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
# output
|
||||
data = {}
|
||||
|
||||
if infos_requested:
|
||||
features, identities, extensions = infos
|
||||
features.sort()
|
||||
identities.sort(key=lambda identity: identity[2])
|
||||
data.update({
|
||||
'features': features,
|
||||
'identities': identities,
|
||||
'extensions': extensions})
|
||||
|
||||
if items_requested:
|
||||
items.sort(key=lambda item: item[2])
|
||||
data['items'] = items
|
||||
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
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])
|
||||
async def start(self):
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid = jids[0]
|
||||
self.host.bridge.getSoftwareVersion(jid, self.host.profile, callback=self.gotVersion, errback=self.error)
|
||||
try:
|
||||
data = await self.host.bridge.getSoftwareVersion(jid, self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(_(f"error while trying to get version: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
infos = []
|
||||
name, version, os = data
|
||||
if name:
|
||||
infos.append(_(f"Software name: {name}"))
|
||||
if version:
|
||||
infos.append(_(f"Software version: {version}"))
|
||||
if os:
|
||||
infos.append(_(f"Operating System: {os}"))
|
||||
|
||||
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(_("Software name: %s") % name)
|
||||
if version:
|
||||
infos.append(_("Software version: %s") % version)
|
||||
if os:
|
||||
infos.append(_("Operating System: %s") % os)
|
||||
|
||||
print("\n".join(infos))
|
||||
self.host.quit()
|
||||
print("\n".join(infos))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Session(base.CommandBase):
|
||||
|
||||
def __init__(self, host):
|
||||
extra_outputs = {'default': self.default_output}
|
||||
super(Session, self).__init__(host, 'session', use_output='dict', extra_outputs=extra_outputs, help=_('running session'))
|
||||
self.need_loop=True
|
||||
|
||||
def default_output(self, data):
|
||||
started = data['started']
|
||||
data['started'] = '{short} (UTC, {relative})'.format(
|
||||
short=date_utils.date_fmt(started),
|
||||
relative=date_utils.date_fmt(started, 'relative'))
|
||||
self.host.output(C.OUTPUT_DICT, 'simple', {}, data)
|
||||
super(Session, self).__init__(
|
||||
host, 'session', use_output='dict', extra_outputs=extra_outputs,
|
||||
help=_('running session'))
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.sessionInfosGet(self.host.profile, callback=self._sessionInfoGetCb, errback=self._sessionInfoGetEb)
|
||||
async def default_output(self, data):
|
||||
started = data['started']
|
||||
data['started'] = '{short} (UTC, {relative})'.format(
|
||||
short=date_utils.date_fmt(started),
|
||||
relative=date_utils.date_fmt(started, 'relative'))
|
||||
await self.host.output(C.OUTPUT_DICT, 'simple', {}, data)
|
||||
|
||||
def _sessionInfoGetCb(self, data):
|
||||
self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
def _sessionInfoGetEb(self, error_data):
|
||||
self.disp(_('Error getting session infos: {}').format(error_data), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
async def start(self):
|
||||
try:
|
||||
data = await self.host.bridge.sessionInfosGet(self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(_(f'Error getting session infos: {e}'), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
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'))
|
||||
super(Info, self).__init__(
|
||||
host, 'info', use_profile=False,
|
||||
help=_('Get various pieces of information on entities'))
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import subprocess
|
||||
import argparse
|
||||
import sys
|
||||
import shlex
|
||||
import asyncio
|
||||
from . 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"
|
||||
|
|
@ -105,10 +107,10 @@ class InputCommon(base.CommandBase):
|
|||
help=_("don't actually run commands but echo what would be launched"),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"--log", type=argparse.FileType("wb"), help=_("log stdout to FILE")
|
||||
"--log", type=argparse.FileType("w"), help=_("log stdout to FILE")
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"--log-err", type=argparse.FileType("wb"), help=_("log stderr to FILE")
|
||||
"--log-err", type=argparse.FileType("w"), help=_("log stderr to FILE")
|
||||
)
|
||||
self.parser.add_argument("command", nargs=argparse.REMAINDER)
|
||||
|
||||
|
|
@ -166,15 +168,15 @@ class InputCommon(base.CommandBase):
|
|||
|
||||
for v in value:
|
||||
if arg_type == OPT_STDIN:
|
||||
self._stdin.append(v.encode("utf-8"))
|
||||
self._stdin.append(v)
|
||||
elif arg_type == OPT_SHORT:
|
||||
self._opts.append("-{}".format(arg_name))
|
||||
self._opts.append(v.encode("utf-8"))
|
||||
self._opts.append(v)
|
||||
elif arg_type == OPT_LONG:
|
||||
self._opts.append("--{}".format(arg_name))
|
||||
self._opts.append(v.encode("utf-8"))
|
||||
self._opts.append(v)
|
||||
elif arg_type == OPT_POS:
|
||||
self._pos.append(v.encode("utf-8"))
|
||||
self._pos.append(v)
|
||||
elif arg_type == OPT_IGNORE:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -184,7 +186,7 @@ class InputCommon(base.CommandBase):
|
|||
).format(type_=arg_type, name=arg_name)
|
||||
)
|
||||
|
||||
def runCommand(self):
|
||||
async def runCommand(self):
|
||||
"""run requested command with parsed arguments"""
|
||||
if self.args_idx != len(self.args.arguments):
|
||||
self.disp(
|
||||
|
|
@ -200,7 +202,10 @@ class InputCommon(base.CommandBase):
|
|||
if self.args.debug:
|
||||
self.disp(
|
||||
A.color(
|
||||
C.A_SUBHEADER, _("values: "), A.RESET, ", ".join(self._values_ori)
|
||||
C.A_SUBHEADER,
|
||||
_("values: "),
|
||||
A.RESET,
|
||||
", ".join([shlex.quote(a) for a in self._values_ori])
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
|
@ -209,34 +214,39 @@ class InputCommon(base.CommandBase):
|
|||
self.disp(A.color(C.A_SUBHEADER, "--- STDIN ---"))
|
||||
self.disp(stdin)
|
||||
self.disp(A.color(C.A_SUBHEADER, "-------------"))
|
||||
|
||||
self.disp(
|
||||
"{indent}{prog} {static} {options} {positionals}".format(
|
||||
indent=4 * " ",
|
||||
prog=sys.argv[0],
|
||||
static=" ".join(self.args.command),
|
||||
options=" ".join([o for o in self._opts]),
|
||||
positionals=" ".join([p for p in self._pos]),
|
||||
options=" ".join(shlex.quote(o) for o in self._opts),
|
||||
positionals=" ".join(shlex.quote(p) for p in self._pos),
|
||||
)
|
||||
)
|
||||
self.disp("\n")
|
||||
else:
|
||||
self.disp(" (" + ", ".join(self._values_ori) + ")", 2, no_lf=True)
|
||||
args = [sys.argv[0]] + self.args.command + self._opts + self._pos
|
||||
p = subprocess.Popen(
|
||||
args,
|
||||
p = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
(stdout, stderr) = p.communicate(stdin)
|
||||
stdout, stderr = await p.communicate(stdin.encode('utf-8'))
|
||||
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))
|
||||
log.write(log_tpl.format(
|
||||
command=" ".join(shlex.quote(a) for a in args),
|
||||
buff=stdout.decode('utf-8', 'replace')))
|
||||
if log_err:
|
||||
log_err.write(log_tpl.format(command=" ".join(args), buff=stderr))
|
||||
ret = p.wait()
|
||||
log_err.write(log_tpl.format(
|
||||
command=" ".join(shlex.quote(a) for a in args),
|
||||
buff=stderr.decode('utf-8', 'replace')))
|
||||
ret = p.returncode
|
||||
if ret == 0:
|
||||
self.disp(A.color(C.A_SUCCESS, _("OK")))
|
||||
else:
|
||||
|
|
@ -307,21 +317,25 @@ class Csv(InputCommon):
|
|||
|
||||
super(Csv, self).filter(filter_type, filter_arg, value)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
import csv
|
||||
|
||||
if self.args.encoding:
|
||||
sys.stdin.reconfigure(encoding=self.args.encoding, errors="replace")
|
||||
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()
|
||||
self.addValue(value)
|
||||
await self.runCommand()
|
||||
except exceptions.CancelError:
|
||||
# this row has been cancelled, we skip it
|
||||
continue
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Input(base.CommandBase):
|
||||
subcommands = (Csv,)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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"]
|
||||
|
||||
|
|
@ -38,7 +37,6 @@ class Create(base.CommandBase):
|
|||
use_output=C.OUTPUT_DICT,
|
||||
help=_("create and send an invitation"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -115,17 +113,7 @@ class Create(base.CommandBase):
|
|||
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(
|
||||
"can't create invitation: {reason}".format(reason=failure_), error=True
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def start(self):
|
||||
async 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:]
|
||||
|
|
@ -139,22 +127,27 @@ class Create(base.CommandBase):
|
|||
_("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,
|
||||
)
|
||||
try:
|
||||
invitation_data = await 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,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create invitation: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(invitation_data)
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
|
|
@ -167,7 +160,6 @@ class Get(base.CommandBase):
|
|||
use_output=C.OUTPUT_DICT,
|
||||
help=_("get invitation data"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -180,52 +172,45 @@ class Get(base.CommandBase):
|
|||
help=_("start profile session and retrieve jid"),
|
||||
)
|
||||
|
||||
def output_data(self, data, jid_=None):
|
||||
async def output_data(self, data, jid_=None):
|
||||
if jid_ is not None:
|
||||
data["jid"] = jid_
|
||||
self.output(data)
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
def invitationGetCb(self, invitation_data):
|
||||
if self.args.with_jid:
|
||||
profile = invitation_data["guest_profile"]
|
||||
async def start(self):
|
||||
try:
|
||||
invitation_data = await self.host.bridge.invitationGet(
|
||||
self.args.id,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(msg=_(f"can't get invitation data: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def session_started(__):
|
||||
self.host.bridge.asyncGetParamA(
|
||||
if not self.args.with_jid:
|
||||
await self.output_data(invitation_data)
|
||||
else:
|
||||
profile = invitation_data["guest_profile"]
|
||||
try:
|
||||
await self.host.bridge.profileStartSession(
|
||||
invitation_data["password"],
|
||||
profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(msg=_(f"can't start session: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
jid_ = await self.host.bridge.asyncGetParamA(
|
||||
"JabberID",
|
||||
"Connection",
|
||||
profile_key=profile,
|
||||
callback=lambda jid_: self.output_data(invitation_data, jid_),
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't retrieve jid: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(msg=_(f"can't retrieve jid: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
self.host.bridge.profileStartSession(
|
||||
invitation_data["password"],
|
||||
profile,
|
||||
callback=session_started,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("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=_("can't get invitation data: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
await self.output_data(invitation_data, jid_)
|
||||
|
||||
|
||||
class Modify(base.CommandBase):
|
||||
|
|
@ -233,7 +218,6 @@ class Modify(base.CommandBase):
|
|||
base.CommandBase.__init__(
|
||||
self, host, "modify", use_profile=False, help=_("modify existing invitation")
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -283,17 +267,7 @@ class Modify(base.CommandBase):
|
|||
"id", help=_("invitation UUID")
|
||||
)
|
||||
|
||||
def invitationModifyCb(self):
|
||||
self.disp(_("invitations have been modified correctly"))
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
def invitationModifyEb(self, failure_):
|
||||
self.disp(
|
||||
"can't create invitation: {reason}".format(reason=failure_), error=True
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
extra = dict(self.args.extra)
|
||||
for arg_name in ("name", "host_name", "email", "language", "profile"):
|
||||
value = getattr(self.args, arg_name)
|
||||
|
|
@ -301,18 +275,23 @@ class Modify(base.CommandBase):
|
|||
continue
|
||||
if arg_name in extra:
|
||||
self.parser.error(
|
||||
_(
|
||||
"you can't set {arg_name} in both optional argument and extra"
|
||||
).format(arg_name=arg_name)
|
||||
_(f"you can't set {arg_name} in both optional argument and extra")
|
||||
)
|
||||
extra[arg_name] = value
|
||||
self.host.bridge.invitationModify(
|
||||
self.args.id,
|
||||
extra,
|
||||
self.args.replace,
|
||||
callback=self.invitationModifyCb,
|
||||
errback=self.invitationModifyEb,
|
||||
)
|
||||
try:
|
||||
await self.host.bridge.invitationModify(
|
||||
self.args.id,
|
||||
extra,
|
||||
self.args.replace,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
f"can't modify invitation: {e}", error=True
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("invitations have been modified successfuly"))
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class List(base.CommandBase):
|
||||
|
|
@ -327,7 +306,6 @@ class List(base.CommandBase):
|
|||
extra_outputs=extra_outputs,
|
||||
help=_("list invitations data"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def default_output(self, data):
|
||||
for idx, datum in enumerate(data.items()):
|
||||
|
|
@ -347,20 +325,17 @@ class List(base.CommandBase):
|
|||
help=_("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=_("can't list invitations: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
async def start(self):
|
||||
try:
|
||||
data = await self.host.bridge.invitationList(
|
||||
self.args.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"return only invitations linked to this profile: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(data)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Invitation(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os.path
|
||||
from . import base
|
||||
from sat.core.i18n import _
|
||||
from sat.tools.common import data_format
|
||||
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,
|
||||
|
|
@ -40,7 +40,6 @@ class Set(base.CommandBase):
|
|||
pubsub_defaults={"service": _("auto"), "node": _("auto")},
|
||||
help=_("publish or update a merge request"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -70,47 +69,43 @@ class Set(base.CommandBase):
|
|||
help=_("labels to categorize your request"),
|
||||
)
|
||||
|
||||
def mergeRequestSetCb(self, published_id):
|
||||
if published_id:
|
||||
self.disp("Merge request published at {pub_id}".format(pub_id=published_id))
|
||||
else:
|
||||
self.disp("Merge request published")
|
||||
self.host.quit(C.EXIT_OK)
|
||||
async def start(self):
|
||||
self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
|
||||
await common.fill_well_known_uri(self, self.repository, "merge requests")
|
||||
if not self.args.force:
|
||||
message = _(
|
||||
f"You are going to publish your changes to service "
|
||||
f"[{self.args.service}], are you sure ?"
|
||||
)
|
||||
await self.host.confirmOrQuit(
|
||||
message, _("merge request publication cancelled"))
|
||||
|
||||
def sendRequest(self):
|
||||
extra = {"update": True} if self.args.item else {}
|
||||
values = {}
|
||||
if self.args.labels is not None:
|
||||
values["labels"] = self.args.labels
|
||||
self.host.bridge.mergeRequestSet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.repository,
|
||||
"auto",
|
||||
values,
|
||||
"",
|
||||
self.args.item,
|
||||
data_format.serialise(extra),
|
||||
self.profile,
|
||||
callback=self.mergeRequestSetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't create merge request: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
published_id = await self.host.bridge.mergeRequestSet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.repository,
|
||||
"auto",
|
||||
values,
|
||||
"",
|
||||
self.args.item,
|
||||
data_format.serialise(extra),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create merge requests: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def askConfirmation(self):
|
||||
if not self.args.force:
|
||||
message = _(
|
||||
"You are going to publish your changes to service [{service}], are you sure ?"
|
||||
).format(service=self.args.service)
|
||||
self.host.confirmOrQuit(message, _("merge request publication cancelled"))
|
||||
self.sendRequest()
|
||||
if published_id:
|
||||
self.disp(_(f"Merge request published at {published_id}"))
|
||||
else:
|
||||
self.disp(_("Merge request published"))
|
||||
|
||||
def start(self):
|
||||
self.repository = os.path.expanduser(os.path.abspath(self.args.repository))
|
||||
common.URIFinder(self, self.repository, "merge requests", self.askConfirmation)
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
|
|
@ -125,45 +120,38 @@ class Get(base.CommandBase):
|
|||
pubsub_defaults={"service": _("auto"), "node": _("auto")},
|
||||
help=_("get a merge request"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def mergeRequestGetCb(self, requests_data):
|
||||
async def start(self):
|
||||
await common.fill_well_known_uri(
|
||||
self, os.getcwd(), "merge requests", meta_map={})
|
||||
extra = {}
|
||||
try:
|
||||
requests_data = await self.host.bridge.mergeRequestsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.max,
|
||||
self.args.items,
|
||||
"",
|
||||
extra,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get merge request: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
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)
|
||||
await xmlui.show(values_only=True)
|
||||
self.disp("")
|
||||
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,
|
||||
"",
|
||||
extra,
|
||||
self.profile,
|
||||
callback=self.mergeRequestGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("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):
|
||||
|
|
@ -176,7 +164,6 @@ class Import(base.CommandBase):
|
|||
pubsub_defaults={"service": _("auto"), "node": _("auto")},
|
||||
help=_("import a merge request"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -187,31 +174,25 @@ class Import(base.CommandBase):
|
|||
help=_("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=_("can't import merge request: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async 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={}
|
||||
)
|
||||
await common.fill_well_known_uri(
|
||||
self, self.repository, "merge requests", meta_map={})
|
||||
extra = {}
|
||||
try:
|
||||
await self.host.bridge.mergeRequestsImport(
|
||||
self.repository,
|
||||
self.args.item,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
extra,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't import merge request: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class MergeRequest(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from sat.core.i18n import _
|
|||
from sat.tools.utils import clean_ustr
|
||||
from sat.tools.common import data_format
|
||||
from sat.tools.common.ansi import ANSI as A
|
||||
from functools import partial
|
||||
|
||||
__commands__ = ["Message"]
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ __commands__ = ["Message"]
|
|||
class Send(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(Send, self).__init__(host, "send", help=_("send a message to a contact"))
|
||||
self.need_loop=True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -84,25 +82,15 @@ class Send(base.CommandBase):
|
|||
"jid", help=_("the destination jid")
|
||||
)
|
||||
|
||||
def multi_send_cb(self):
|
||||
self.sent += 1
|
||||
if self.sent == self.to_send:
|
||||
self.host.quit(self.errcode)
|
||||
|
||||
def multi_send_eb(self, failure_, msg):
|
||||
self.disp(_("Can't send message [{msg}]: {reason}").format(
|
||||
msg=msg, reason=failure_))
|
||||
self.errcode = C.EXIT_BRIDGE_ERRBACK
|
||||
self.multi_send_cb()
|
||||
|
||||
def sendStdin(self, dest_jid):
|
||||
async 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 ""
|
||||
# FIXME: stdin is not read asynchronously at the moment
|
||||
stdin_lines = [
|
||||
stream.decode("utf-8", "ignore") for stream in sys.stdin.readlines()
|
||||
stream for stream in sys.stdin.readlines()
|
||||
]
|
||||
extra = {}
|
||||
if self.args.subject is None:
|
||||
|
|
@ -113,99 +101,98 @@ class Send(base.CommandBase):
|
|||
if self.args.xhtml or self.args.rich:
|
||||
key = "xhtml" if self.args.xhtml else "rich"
|
||||
if self.args.lang:
|
||||
key = "{}_{}".format(key, self.args.lang)
|
||||
key = f"{key}_{self.args.lang}"
|
||||
extra[key] = clean_ustr("".join(stdin_lines))
|
||||
stdin_lines = []
|
||||
|
||||
if self.args.separate: # we send stdin in several messages
|
||||
self.to_send = 0
|
||||
self.sent = 0
|
||||
self.errcode = 0
|
||||
to_send = []
|
||||
|
||||
error = False
|
||||
|
||||
if self.args.separate:
|
||||
# we send stdin in several messages
|
||||
if header:
|
||||
self.to_send += 1
|
||||
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,
|
||||
)
|
||||
# first we sent the header
|
||||
try:
|
||||
await self.host.bridge.messageSend(
|
||||
dest_jid,
|
||||
{self.args.lang: header},
|
||||
subject,
|
||||
self.args.type,
|
||||
profile_key=self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't send header: {e}", error=True)
|
||||
error = True
|
||||
|
||||
self.to_send += len(stdin_lines)
|
||||
for line in stdin_lines:
|
||||
self.host.bridge.messageSend(
|
||||
to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))}
|
||||
for l in stdin_lines)
|
||||
else:
|
||||
# we sent all in a single message
|
||||
if not (self.args.xhtml or self.args.rich):
|
||||
msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))}
|
||||
else:
|
||||
msg = {}
|
||||
to_send.append(msg)
|
||||
|
||||
for msg in to_send:
|
||||
try:
|
||||
await self.host.bridge.messageSend(
|
||||
dest_jid,
|
||||
{self.args.lang: line.replace("\n", "")},
|
||||
msg,
|
||||
subject,
|
||||
self.args.type,
|
||||
extra,
|
||||
profile_key=self.host.profile,
|
||||
callback=self.multi_send_cb,
|
||||
errback=partial(self.multi_send_eb, msg=line),
|
||||
)
|
||||
profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't send message {msg!r}: {e}", error=True)
|
||||
error = True
|
||||
|
||||
else:
|
||||
msg = (
|
||||
{self.args.lang: header + clean_ustr("".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=self.host.quit,
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't send message: {}")))
|
||||
if error:
|
||||
# at least one message sending failed
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def encryptionNamespaceGetCb(self, namespace, jid_):
|
||||
self.host.bridge.messageEncryptionStart(
|
||||
jid_, namespace, not self.args.encrypt_noreplace,
|
||||
self.profile,
|
||||
callback=lambda: self.sendStdin(jid_),
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't start encryption session: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.xhtml and self.args.separate:
|
||||
self.disp(
|
||||
"argument -s/--separate is not compatible yet with argument -x/--xhtml",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(2)
|
||||
self.host.quit(C.EXIT_BAD_ARG)
|
||||
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
jids = await self.host.check_jids([self.args.jid])
|
||||
jid_ = jids[0]
|
||||
|
||||
if self.args.encrypt_noreplace and self.args.encrypt is None:
|
||||
self.parser.error("You need to use --encrypt if you use --encrypt-noreplace")
|
||||
|
||||
if self.args.encrypt is not None:
|
||||
self.host.bridge.encryptionNamespaceGet(self.args.encrypt,
|
||||
callback=partial(self.encryptionNamespaceGetCb, jid_=jid_),
|
||||
errback=partial(self.errback,
|
||||
msg=_("Can't get encryption namespace: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
else:
|
||||
self.sendStdin(jid_)
|
||||
try:
|
||||
namespace = await self.host.bridge.encryptionNamespaceGet(
|
||||
self.args.encrypt)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get encryption namespace: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
await self.host.bridge.messageEncryptionStart(
|
||||
jid_, namespace, not self.args.encrypt_noreplace, self.profile
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't start encryption session: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
await self.sendStdin(jid_)
|
||||
|
||||
|
||||
class MAM(base.CommandBase):
|
||||
|
||||
def __init__(self, host):
|
||||
super(MAM, self).__init__(
|
||||
host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, help=_("query archives using MAM"))
|
||||
self.need_loop=True
|
||||
host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True,
|
||||
help=_("query archives using MAM"))
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -235,28 +222,7 @@ class MAM(base.CommandBase):
|
|||
"--index", dest="rsm_index", type=int,
|
||||
help=_("index of the page to retrieve"))
|
||||
|
||||
def _sessionInfosGetCb(self, session_info, data, metadata):
|
||||
self.host.own_jid = jid.JID(session_info["jid"])
|
||||
self.output(data)
|
||||
# FIXME: metadata are not displayed correctly and don't play nice with output
|
||||
# they should be added to output data somehow
|
||||
if self.verbosity:
|
||||
for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count",
|
||||
"mam_complete", "mam_stable"):
|
||||
if value in metadata:
|
||||
label = value.split("_")[1]
|
||||
self.disp(A.color(
|
||||
C.A_HEADER, label, ': ' , A.RESET, metadata[value]))
|
||||
|
||||
self.host.quit()
|
||||
|
||||
def _MAMGetCb(self, result):
|
||||
data, metadata, profile = result
|
||||
self.host.bridge.sessionInfosGet(self.profile,
|
||||
callback=partial(self._sessionInfosGetCb, data=data, metadata=metadata),
|
||||
errback=self.errback)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
extra = {}
|
||||
if self.args.mam_start is not None:
|
||||
extra["mam_start"] = float(self.args.mam_start)
|
||||
|
|
@ -269,9 +235,35 @@ class MAM(base.CommandBase):
|
|||
value = getattr(self.args,key)
|
||||
if value is not None:
|
||||
extra[key] = str(value)
|
||||
self.host.bridge.MAMGet(
|
||||
self.args.service, data_format.serialise(extra), self.profile,
|
||||
callback=self._MAMGetCb, errback=self.errback)
|
||||
try:
|
||||
data, metadata, profile = await self.host.bridge.MAMGet(
|
||||
self.args.service, data_format.serialise(extra), self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't retrieve MAM archives: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
session_info = await self.host.bridge.sessionInfosGet(self.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get session infos: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
# we need to fill own_jid for message output
|
||||
self.host.own_jid = jid.JID(session_info["jid"])
|
||||
|
||||
await self.output(data)
|
||||
|
||||
# FIXME: metadata are not displayed correctly and don't play nice with output
|
||||
# they should be added to output data somehow
|
||||
if self.verbosity:
|
||||
for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count",
|
||||
"mam_complete", "mam_stable"):
|
||||
if value in metadata:
|
||||
label = value.split("_")[1]
|
||||
self.disp(A.color(
|
||||
C.A_HEADER, label, ': ' , A.RESET, metadata[value]))
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Message(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -21,87 +21,118 @@
|
|||
|
||||
from . import base
|
||||
from sat.core.i18n import _
|
||||
from .constants import Const as C
|
||||
__commands__ = ["Param"]
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(Get, self).__init__(host, 'get', need_connect=False, help=_('Get a parameter value'))
|
||||
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='?', help=_("Category of the parameter"))
|
||||
self.parser.add_argument("name", nargs='?', help=_("Name of the parameter"))
|
||||
self.parser.add_argument("-a", "--attribute", type=str, default="value", help=_("Name of the attribute to get"))
|
||||
self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit"))
|
||||
self.parser.add_argument(
|
||||
"category", nargs='?', help=_("category of the parameter"))
|
||||
self.parser.add_argument("name", nargs='?', help=_("name of the parameter"))
|
||||
self.parser.add_argument(
|
||||
"-a", "--attribute", type=str, default="value",
|
||||
help=_("name of the attribute to get"))
|
||||
self.parser.add_argument(
|
||||
"--security-limit", type=int, default=-1, help=_("security limit"))
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.category is None:
|
||||
categories = self.host.bridge.getParamsCategories()
|
||||
categories = await self.host.bridge.getParamsCategories()
|
||||
print("\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)
|
||||
values_dict = await self.host.bridge.asyncGetParamsValuesFromCategory(
|
||||
self.args.category, self.args.security_limit, self.profile)
|
||||
except Exception as e:
|
||||
print("Can't find requested parameters: {}".format(e))
|
||||
self.host.quit(1)
|
||||
for name, value in values_dict.items():
|
||||
print("{}\t{}".format(name, value))
|
||||
self.disp(_(f"can't find requested parameters: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
else:
|
||||
for name, value in values_dict.items():
|
||||
print(f"{name}\t{value}")
|
||||
else:
|
||||
try:
|
||||
value = self.host.bridge.asyncGetParamA(self.args.name, self.args.category, self.args.attribute,
|
||||
self.args.security_limit, self.profile)
|
||||
value = await self.host.bridge.asyncGetParamA(
|
||||
self.args.name, self.args.category, self.args.attribute,
|
||||
self.args.security_limit, self.profile)
|
||||
except Exception as e:
|
||||
print("Can't find requested parameter: {}".format(e))
|
||||
self.host.quit(1)
|
||||
print(value)
|
||||
self.disp(_(f"can't find requested parameter: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
else:
|
||||
print(value)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Set(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(Set, self).__init__(host, 'set', need_connect=False, help=_('Set a parameter value'))
|
||||
super(Set, self).__init__(host, 'set', need_connect=False, help=_('set a parameter value'))
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument("category", help=_("Category of the parameter"))
|
||||
self.parser.add_argument("name", help=_("Name of the parameter"))
|
||||
self.parser.add_argument("value", help=_("Name of the parameter"))
|
||||
self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit"))
|
||||
self.parser.add_argument("category", help=_("category of the parameter"))
|
||||
self.parser.add_argument("name", help=_("name of the parameter"))
|
||||
self.parser.add_argument("value", help=_("name of the parameter"))
|
||||
self.parser.add_argument("--security-limit", type=int, default=-1, help=_("security limit"))
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
try:
|
||||
self.host.bridge.setParam(self.args.name, self.args.value, self.args.category, self.args.security_limit, self.profile)
|
||||
await self.host.bridge.setParam(
|
||||
self.args.name, self.args.value, self.args.category,
|
||||
self.args.security_limit, self.profile)
|
||||
except Exception as e:
|
||||
print("Can set requested parameter: {}".format(e))
|
||||
self.disp(_(f"can't set requested parameter: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class SaveTemplate(base.CommandBase):
|
||||
# FIXME: this should probably be removed, it's not used and not useful for end-user
|
||||
|
||||
def __init__(self, host):
|
||||
super(SaveTemplate, self).__init__(host, 'save', use_profile=False, help=_('Save parameters template to xml file'))
|
||||
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"))
|
||||
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)
|
||||
async def start(self):
|
||||
"""Save parameters template to XML file"""
|
||||
try:
|
||||
await self.host.bridge.saveParamsTemplate(self.args.filename)
|
||||
except Exception as e:
|
||||
self.disp(_(f"can't save parameters to file: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
print(_("Can't save parameters to file %s") % self.args.filename)
|
||||
self.disp(_(f"parameters saved to file {self.args.filename}"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class LoadTemplate(base.CommandBase):
|
||||
# FIXME: this should probably be removed, it's not used and not useful for end-user
|
||||
|
||||
def __init__(self, host):
|
||||
super(LoadTemplate, self).__init__(host, 'load', use_profile=False, help=_('Load parameters template from xml file'))
|
||||
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"))
|
||||
self.parser.add_argument("filename", type=str, help=_("input file"))
|
||||
|
||||
def start(self):
|
||||
async 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)
|
||||
try:
|
||||
self.host.bridge.loadParamsTemplate(self.args.filename)
|
||||
except Exception as e:
|
||||
self.disp(_(f"can't load parameters from file: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
print(_("Can't load parameters from file %s") % self.args.filename)
|
||||
self.disp(_(f"parameters loaded from file {self.args.filename}"))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Param(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
from . import base
|
||||
from sat.core.i18n import _
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
|
||||
__commands__ = ["Ping"]
|
||||
|
||||
|
|
@ -27,7 +28,6 @@ class Ping(base.CommandBase):
|
|||
|
||||
def __init__(self, host):
|
||||
super(Ping, self).__init__(host, 'ping', help=_('ping XMPP entity'))
|
||||
self.need_loop=True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -37,11 +37,13 @@ class Ping(base.CommandBase):
|
|||
"-d", "--delay-only", action="store_true", help=_("output delay only (in s)")
|
||||
)
|
||||
|
||||
def _pingCb(self, pong_time):
|
||||
fmt = "{time}" if self.args.delay_only else "PONG ({time} s)"
|
||||
self.disp(fmt.format(time=pong_time))
|
||||
self.host.quit()
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.ping(self.args.jid, self.profile,
|
||||
callback=self._pingCb, errback=self.errback)
|
||||
async def start(self):
|
||||
try:
|
||||
pong_time = await self.host.bridge.ping(self.args.jid, self.profile)
|
||||
except Exception as e:
|
||||
self.disp(msg=_(f"can't do the ping: {e}"), error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
msg = pong_time if self.args.delay_only else f"PONG ({pong_time} s)"
|
||||
self.disp(msg)
|
||||
self.host.quit()
|
||||
|
|
|
|||
|
|
@ -17,17 +17,16 @@
|
|||
# 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 socket
|
||||
import asyncio
|
||||
import errno
|
||||
from functools import partial
|
||||
from sat_frontends.jp import base
|
||||
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from sat_frontends.jp import xmlui_manager
|
||||
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"]
|
||||
|
||||
|
|
@ -37,59 +36,60 @@ 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", 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):
|
||||
async 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=_("can't start stream: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
port = await self.host.bridge.streamOut(
|
||||
await self.host.get_full_jid(self.args.jid),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't start stream: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
# FIXME: we use temporarily blocking code here, as it simplify
|
||||
# asyncio port: "loop.connect_read_pipe(lambda: reader_protocol,
|
||||
# sys.stdin.buffer)" doesn't work properly when a file is piped in
|
||||
# (we get a "ValueError: Pipe transport is for pipes/sockets only.")
|
||||
# while it's working well for simple text sending.
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(("127.0.0.1", int(port)))
|
||||
|
||||
while True:
|
||||
buf = sys.stdin.buffer.read(4096)
|
||||
if not buf:
|
||||
break
|
||||
try:
|
||||
s.sendall(buf)
|
||||
except socket.error as e:
|
||||
if e.errno == errno.EPIPE:
|
||||
sys.stderr.write(f"e\n")
|
||||
self.host.quit(1)
|
||||
else:
|
||||
raise e
|
||||
self.host.quit()
|
||||
|
||||
|
||||
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
|
||||
async def handle_stream_in(reader, writer, host):
|
||||
"""Write all received data to stdout"""
|
||||
while True:
|
||||
data = await reader.read(4096)
|
||||
if not data:
|
||||
break
|
||||
sys.stdout.buffer.write(data)
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except IOError as e:
|
||||
sys.stderr.write(f"{e}\n")
|
||||
break
|
||||
host.quitFromSignal()
|
||||
|
||||
|
||||
class PipeIn(base.CommandAnswering):
|
||||
|
|
@ -105,35 +105,33 @@ class PipeIn(base.CommandAnswering):
|
|||
)
|
||||
|
||||
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(_("Action has no XMLUI"), 1)
|
||||
else:
|
||||
ui = ET.fromstring(xml_ui.encode("utf-8"))
|
||||
xmlui_id = ui.get("submit")
|
||||
if not xmlui_id:
|
||||
ui = xmlui_manager.create(self.host, xml_ui)
|
||||
if not ui.submit_id:
|
||||
self.disp(_("Invalid XMLUI received"), error=True)
|
||||
return xmlui_id
|
||||
self.quitFromSignal(C.EXIT_INTERNAL_ERROR)
|
||||
return ui.submit_id
|
||||
|
||||
def onStreamAction(self, action_data, action_id, security_limit, profile):
|
||||
async 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)
|
||||
self.host.quitFromSignal(C.EXIT_ERROR)
|
||||
try:
|
||||
from_jid = jid.JID(action_data["meta_from_jid"])
|
||||
except KeyError:
|
||||
self.disp(_("Ignoring action without from_jid data"), 1)
|
||||
self.disp(_("Ignoring action without from_jid data"), error=True)
|
||||
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)
|
||||
server = await asyncio.start_server(
|
||||
partial(handle_stream_in, host=self.host), host, port)
|
||||
except socket.error as e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
port += 1
|
||||
|
|
@ -142,12 +140,15 @@ class PipeIn(base.CommandAnswering):
|
|||
else:
|
||||
break
|
||||
xmlui_data = {"answer": C.BOOL_TRUE, "port": str(port)}
|
||||
self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
|
||||
server.serve_forever()
|
||||
await self.host.bridge.launchAction(
|
||||
xmlui_id, xmlui_data, profile_key=profile)
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
self.host.quitFromSignal()
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
|
||||
await self.start_answering()
|
||||
|
||||
|
||||
class Pipe(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ 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"]
|
||||
|
||||
|
|
@ -43,18 +42,73 @@ class ProfileConnect(base.CommandBase):
|
|||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
async def start(self):
|
||||
# connection is already managed by profile common commands
|
||||
# so we just need to check arguments and quit
|
||||
if not self.args.connect and not self.args.start_session:
|
||||
self.parser.error(_("You need to use either --connect or --start-session"))
|
||||
self.host.quit()
|
||||
|
||||
class ProfileDisconnect(base.CommandBase):
|
||||
|
||||
def __init__(self, host):
|
||||
super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=('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)
|
||||
async def start(self):
|
||||
try:
|
||||
await self.host.bridge.disconnect(self.args.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't disconnect profile: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class ProfileCreate(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=('create a new profile'))
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument('profile', type=str, help=_('the name of the profile'))
|
||||
self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile'))
|
||||
self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
|
||||
self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
|
||||
metavar='PASSWORD')
|
||||
self.parser.add_argument('-C', '--component', default='',
|
||||
help=_('set to component import name (entry point) if this is a component'))
|
||||
|
||||
async def start(self):
|
||||
"""Create a new profile"""
|
||||
if self.args.profile in await self.host.bridge.profilesListGet():
|
||||
self.disp(f"Profile {self.args.profile} already exists.", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERROR)
|
||||
try:
|
||||
await self.host.bridge.profileCreate(
|
||||
self.args.profile, self.args.password, self.args.component)
|
||||
except Exception as e:
|
||||
self.disp(f"can't create profile: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
await self.host.bridge.profileStartSession(
|
||||
self.args.password, self.args.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't start profile session: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
if self.args.jid:
|
||||
await 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:
|
||||
await self.host.bridge.setParam(
|
||||
"Password", xmpp_pwd, "Connection", profile_key=self.args.profile)
|
||||
|
||||
self.disp(f'profile {self.args.profile} created successfully', 1)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class ProfileDefault(base.CommandBase):
|
||||
|
|
@ -64,8 +118,9 @@ class ProfileDefault(base.CommandBase):
|
|||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
print(self.host.bridge.profileNameGet('@DEFAULT@'))
|
||||
async def start(self):
|
||||
print(await self.host.bridge.profileNameGet('@DEFAULT@'))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class ProfileDelete(base.CommandBase):
|
||||
|
|
@ -76,47 +131,51 @@ class ProfileDelete(base.CommandBase):
|
|||
self.parser.add_argument('profile', type=str, help=PROFILE_HELP)
|
||||
self.parser.add_argument('-f', '--force', action='store_true', help=_('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)
|
||||
async def start(self):
|
||||
if self.args.profile not in await self.host.bridge.profilesListGet():
|
||||
log.error(f"Profile {self.args.profile} doesn't exist.")
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
if not self.args.force:
|
||||
message = "Are you sure to delete profile [{}] ?".format(self.args.profile)
|
||||
res = input("{} (y/N)? ".format(message))
|
||||
if res not in ("y", "Y"):
|
||||
self.disp(_("Profile deletion cancelled"))
|
||||
self.host.quit(2)
|
||||
message = f"Are you sure to delete profile [{self.args.profile}] ?"
|
||||
cancel_message = "Profile deletion cancelled"
|
||||
await self.host.confirmOrQuit(message, cancel_message)
|
||||
|
||||
self.host.bridge.asyncDeleteProfile(self.args.profile, callback=lambda __: None)
|
||||
await self.host.bridge.asyncDeleteProfile(self.args.profile)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class ProfileInfo(base.CommandBase):
|
||||
def __init__(self, host):
|
||||
super(ProfileInfo, self).__init__(host, 'info', need_connect=False, help=_('get information about a profile'))
|
||||
self.need_loop = True
|
||||
self.to_show = [(_("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=_('show the XMPP password IN CLEAR TEXT'))
|
||||
|
||||
def showNextValue(self, label=None, category=None, value=None):
|
||||
async 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((("{label:<"+str(self.largest+2)+"}{value}").format(label=label+": ", value=value)))
|
||||
print((("{label:<"+str(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))
|
||||
try:
|
||||
value = await self.host.bridge.asyncGetParamA(
|
||||
name, category, profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get {name}/{category} param: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.showNextValue(label, category, value)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.show_password:
|
||||
self.to_show.append((_("XMPP password"), "Connection", "Password"))
|
||||
self.showNextValue()
|
||||
await self.showNextValue()
|
||||
|
||||
|
||||
class ProfileList(base.CommandBase):
|
||||
|
|
@ -129,54 +188,19 @@ class ProfileList(base.CommandBase):
|
|||
group.add_argument('-C', '--components', action='store_true', help=('get components profiles only'))
|
||||
|
||||
|
||||
def start(self):
|
||||
async 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=('create a new profile'))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument('profile', type=str, help=_('the name of the profile'))
|
||||
self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile'))
|
||||
self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
|
||||
self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
|
||||
metavar='PASSWORD')
|
||||
self.parser.add_argument('-C', '--component', default='',
|
||||
help=_('set to component import name (entry point) if this is a component'))
|
||||
|
||||
def _session_started(self, __):
|
||||
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)
|
||||
await self.output(await self.host.bridge.profilesListGet(clients, components))
|
||||
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=_("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=_('modify an existing profile'))
|
||||
|
||||
|
|
@ -189,17 +213,22 @@ class ProfileModify(base.CommandBase):
|
|||
metavar='PASSWORD')
|
||||
self.parser.add_argument('-D', '--default', action='store_true', help=_('set as default profile'))
|
||||
|
||||
def start(self):
|
||||
async 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)
|
||||
await 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)
|
||||
await 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)
|
||||
await 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)
|
||||
await self.host.bridge.profileSetDefault(self.host.profile)
|
||||
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Profile(base.CommandBase):
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -20,77 +20,67 @@
|
|||
|
||||
from . import base
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
from sat.core.i18n import _
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from sat_frontends.tools import jid
|
||||
from sat.tools.common.ansi import ANSI as A
|
||||
|
||||
__commands__ = ["Roster"]
|
||||
|
||||
|
||||
|
||||
class Purge(base.CommandBase):
|
||||
class Get(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
|
||||
super().__init__(
|
||||
host, 'get', use_output=C.OUTPUT_DICT, use_verbose=True,
|
||||
extra_outputs = {"default": self.default_output},
|
||||
help=_('retrieve the roster entities'))
|
||||
|
||||
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"))
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
|
||||
def default_output(self, data):
|
||||
for contact_jid, contact_data in data.items():
|
||||
all_keys = list(contact_data.keys())
|
||||
keys_to_show = []
|
||||
name = contact_data.get('name', contact_jid.node)
|
||||
|
||||
def error(self, failure):
|
||||
print((_("Error while retrieving the contacts [%s]") % failure))
|
||||
self.host.quit(1)
|
||||
if self.verbosity >= 1:
|
||||
keys_to_show.append('groups')
|
||||
all_keys.remove('groups')
|
||||
if self.verbosity >= 2:
|
||||
keys_to_show.extend(all_keys)
|
||||
|
||||
def ask_confirmation(self, no_sub, no_from, no_to):
|
||||
"""Ask the confirmation before removing contacts.
|
||||
if name is None:
|
||||
self.disp(A.color(C.A_HEADER, contact_jid))
|
||||
else:
|
||||
self.disp(A.color(C.A_HEADER, name, A.RESET, f" ({contact_jid})"))
|
||||
for k in keys_to_show:
|
||||
value = contact_data[k]
|
||||
if value:
|
||||
if isinstance(value, list):
|
||||
value = ', '.join(value)
|
||||
self.disp(A.color(
|
||||
" ", C.A_SUBHEADER, f"{k}: ", A.RESET, str(value)))
|
||||
|
||||
@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 = input("%s (y/N)? " % message)
|
||||
if not res or res.lower() == 'n':
|
||||
return False
|
||||
if res.lower() == 'y':
|
||||
return True
|
||||
async def start(self):
|
||||
try:
|
||||
contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"error while retrieving the contacts: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
def gotContacts(self, contacts):
|
||||
"""Process the list of contacts.
|
||||
contacts_dict = {}
|
||||
for contact_jid_s, data, groups in contacts:
|
||||
# FIXME: we have to convert string to bool here for historical reason
|
||||
# getContacts format should be changed and serialised properly
|
||||
for key in ('from', 'to', 'ask'):
|
||||
if key in data:
|
||||
data[key] = C.bool(data[key])
|
||||
data['groups'] = list(groups)
|
||||
contacts_dict[jid.JID(contact_jid_s)] = data
|
||||
|
||||
@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 __: None, errback=lambda failure: None)
|
||||
await self.output(contacts_dict)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
|
|
@ -98,23 +88,17 @@ 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)
|
||||
async def start(self):
|
||||
try:
|
||||
contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"error while retrieving the contacts: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
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
|
||||
|
|
@ -127,7 +111,9 @@ class Stats(base.CommandBase):
|
|||
no_from += 1
|
||||
elif not to:
|
||||
no_to += 1
|
||||
host = jid.JID(contact).host
|
||||
|
||||
host = jid.JID(contact).domain
|
||||
|
||||
hosts.setdefault(host, 0)
|
||||
hosts[host] += 1
|
||||
if groups:
|
||||
|
|
@ -142,7 +128,8 @@ class Stats(base.CommandBase):
|
|||
print("Number of different hosts: %d" % len(hosts))
|
||||
print()
|
||||
for host, count in hosts.items():
|
||||
print("Contacts on {host}: {count} ({rate:.1f}%)".format(host=host, count=count, rate=100 * float(count) / len(contacts)))
|
||||
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)
|
||||
|
|
@ -158,81 +145,115 @@ class Stats(base.CommandBase):
|
|||
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(f"Average groups' subscriptions per contact: {groups_per_contact:.1f}")
|
||||
print("Contacts not assigned to any group: %d" % no_group)
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Get(base.CommandBase):
|
||||
class Purge(base.CommandBase):
|
||||
|
||||
def __init__(self, host):
|
||||
super(Get, self).__init__(host, 'get', help=_('Retrieve the roster contacts'))
|
||||
self.need_loop = True
|
||||
super(Purge, self).__init__(
|
||||
host, 'purge',
|
||||
help=_('purge the roster from its contacts with no subscription'))
|
||||
|
||||
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"))
|
||||
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)
|
||||
async def start(self):
|
||||
try:
|
||||
contacts = await self.host.bridge.getContacts(self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"error while retrieving the contacts: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
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
|
||||
no_sub, no_from, no_to = [], [], []
|
||||
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(str(attrs.get("name", "")))
|
||||
if self.args.groups:
|
||||
args.append("\t".join(groups) if groups else "")
|
||||
print(";".join(["{}"] * field_count).format(*args).encode("utf-8"))
|
||||
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:
|
||||
self.disp(
|
||||
f"Nothing to do - there's a from and/or to subscription(s) between "
|
||||
f"profile {self.host.profile!r} and each of its contacts"
|
||||
)
|
||||
elif await self.ask_confirmation(no_sub, no_from, no_to):
|
||||
for contact in no_sub + no_from + no_to:
|
||||
try:
|
||||
await self.host.bridge.delContact(
|
||||
contact, profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't delete contact {contact!r}: {e}", error=True)
|
||||
else:
|
||||
self.disp(f"contact {contact!r} has been removed")
|
||||
|
||||
self.host.quit()
|
||||
|
||||
async 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:
|
||||
self.disp(
|
||||
f"There's no subscription between profile {self.host.profile!r} and the "
|
||||
f"following contacts:")
|
||||
self.disp(" " + "\n ".join(no_sub))
|
||||
if no_from:
|
||||
self.disp(
|
||||
f"There's no 'from' subscription between profile {self.host.profile!r} "
|
||||
f"and the following contacts:")
|
||||
self.disp(" " + "\n ".join(no_from))
|
||||
if no_to:
|
||||
self.disp(
|
||||
f"There's no 'to' subscription between profile {self.host.profile!r} and "
|
||||
f"the following contacts:")
|
||||
self.disp(" " + "\n ".join(no_to))
|
||||
message = f"REMOVE them from profile {self.host.profile}'s roster"
|
||||
while True:
|
||||
res = await self.host.ainput(f"{message} (y/N)? ")
|
||||
if not res or res.lower() == 'n':
|
||||
return False
|
||||
if res.lower() == 'y':
|
||||
return True
|
||||
|
||||
|
||||
class Resync(base.CommandBase):
|
||||
|
||||
def __init__(self, host):
|
||||
super(Resync, self).__init__(
|
||||
host, 'resync', help=_('do a full resynchronisation of roster with server'))
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def rosterResyncCb(self):
|
||||
self.disp(_("Roster resynchronized"))
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
def start(self):
|
||||
self.host.bridge.rosterResync(profile_key=self.host.profile,
|
||||
callback=self.rosterResyncCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't resynchronise roster: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
))
|
||||
async def start(self):
|
||||
try:
|
||||
await self.host.bridge.rosterResync(profile_key=self.host.profile)
|
||||
except Exception as e:
|
||||
self.disp(f"can't resynchronise roster: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(_("Roster resynchronized"))
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Roster(base.CommandBase):
|
||||
subcommands = (Get, Stats, Purge, Resync)
|
||||
|
||||
def __init__(self, host):
|
||||
super(Roster, self).__init__(host, 'roster', use_profile=True, help=_("Manage an entity's roster"))
|
||||
super(Roster, self).__init__(
|
||||
host, 'roster', use_profile=True, help=_("Manage an entity's roster"))
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from . import base
|
||||
import cmd
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
from . 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 arg_tools
|
||||
from sat.tools.common.ansi import ANSI as A
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
__commands__ = ["Shell"]
|
||||
INTRO = _(
|
||||
|
|
@ -44,7 +44,8 @@ Enter "help" or "?" to know what to do
|
|||
class Shell(base.CommandBase, cmd.Cmd):
|
||||
def __init__(self, host):
|
||||
base.CommandBase.__init__(
|
||||
self, host, "shell", help=_("launch jp in shell (REPL) mode")
|
||||
self, host, "shell",
|
||||
help=_("launch jp in shell (REPL) mode")
|
||||
)
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
|
|
@ -152,13 +153,15 @@ class Shell(base.CommandBase, cmd.Cmd):
|
|||
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()
|
||||
# FIXME: debug crashes on exit and is not that useful,
|
||||
# keeping it until refactoring, may be removed entirely then
|
||||
# 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"""
|
||||
|
|
@ -188,10 +191,7 @@ class Shell(base.CommandBase, cmd.Cmd):
|
|||
|
||||
def do_version(self, args):
|
||||
"""show current SàT/jp version"""
|
||||
try:
|
||||
self.host.run(["--version"])
|
||||
except SystemExit:
|
||||
pass
|
||||
self.run_cmd(['--version'])
|
||||
|
||||
def do_shell(self, args):
|
||||
"""launch an external command (you can use ![command] too)"""
|
||||
|
|
@ -292,7 +292,9 @@ class Shell(base.CommandBase, cmd.Cmd):
|
|||
"""alias for quit"""
|
||||
self.do_quit(args)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
# FIXME: "shell" is currently kept synchronous as it works well as it
|
||||
# and it will be refactored soon.
|
||||
default_profile = self.host.bridge.profileNameGet(C.PROF_KEY_DEFAULT)
|
||||
self._not_default_profile = self.profile != default_profile
|
||||
self.path = []
|
||||
|
|
@ -300,4 +302,4 @@ class Shell(base.CommandBase, cmd.Cmd):
|
|||
self.use = {}
|
||||
self.verbose = False
|
||||
self.update_path()
|
||||
self.cmdloop(INTRO.encode("utf-8"))
|
||||
self.cmdloop(INTRO)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from . import base
|
|||
from sat.core.i18n import _
|
||||
from sat_frontends.jp import common
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from functools import partial
|
||||
import json
|
||||
import os
|
||||
|
||||
|
|
@ -44,37 +43,31 @@ class Get(base.CommandBase):
|
|||
use_output=C.OUTPUT_LIST_XMLUI,
|
||||
help=_("get tickets"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
pass
|
||||
|
||||
def ticketsGetCb(self, tickets_data):
|
||||
self.output(tickets_data[0])
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
def getTickets(self):
|
||||
self.host.bridge.ticketsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.max,
|
||||
self.args.items,
|
||||
"",
|
||||
self.getPubsubExtra(),
|
||||
self.profile,
|
||||
callback=self.ticketsGetCb,
|
||||
errback=partial(
|
||||
self.errback,
|
||||
msg=_("can't get tickets: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
common.URIFinder(self, os.getcwd(), "tickets", self.getTickets, meta_map={})
|
||||
async def start(self):
|
||||
await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={})
|
||||
try:
|
||||
tickets_data = await self.host.bridge.ticketsGet(
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.args.max,
|
||||
self.args.items,
|
||||
"",
|
||||
self.getPubsubExtra(),
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't get tickets: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.output(tickets_data[0])
|
||||
self.host.quit(C.EXIT_OK)
|
||||
|
||||
|
||||
class Import(base.CommandAnswering):
|
||||
class Import(base.CommandBase):
|
||||
# TODO: factorize with blog/import
|
||||
|
||||
def __init__(self, host):
|
||||
|
|
@ -82,9 +75,9 @@ class Import(base.CommandAnswering):
|
|||
host,
|
||||
"import",
|
||||
use_progress=True,
|
||||
use_verbose=True,
|
||||
help=_("import tickets from external software/dataset"),
|
||||
)
|
||||
self.need_loop = True
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument(
|
||||
|
|
@ -109,7 +102,8 @@ class Import(base.CommandAnswering):
|
|||
default=[],
|
||||
metavar=("IMPORTED_FIELD", "DEST_FIELD"),
|
||||
help=_(
|
||||
"specified field in import data will be put in dest field (default: use same field name, or ignore if it doesn't exist)"
|
||||
"specified field in import data will be put in dest field (default: use "
|
||||
"same field name, or ignore if it doesn't exist)"
|
||||
),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
|
|
@ -125,97 +119,92 @@ class Import(base.CommandAnswering):
|
|||
default="",
|
||||
metavar="PUBSUB_NODE",
|
||||
help=_(
|
||||
"PubSub node where the items must be uploaded (default: tickets' defaults)"
|
||||
"PubSub node where the items must be uploaded (default: tickets' "
|
||||
"defaults)"
|
||||
),
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"location",
|
||||
nargs="?",
|
||||
help=_(
|
||||
"importer data location (see importer description), nothing to show importer description"
|
||||
"importer data location (see importer description), nothing to show "
|
||||
"importer description"
|
||||
),
|
||||
)
|
||||
|
||||
def onProgressStarted(self, metadata):
|
||||
async def onProgressStarted(self, metadata):
|
||||
self.disp(_("Tickets upload started"), 2)
|
||||
|
||||
def onProgressFinished(self, metadata):
|
||||
async def onProgressFinished(self, metadata):
|
||||
self.disp(_("Tickets uploaded successfully"), 2)
|
||||
|
||||
def onProgressError(self, error_msg):
|
||||
self.disp(_("Error while uploading tickets: {}").format(error_msg), error=True)
|
||||
async def onProgressError(self, error_msg):
|
||||
self.disp(_(f"Error while uploading tickets: {error_msg}"), error=True)
|
||||
|
||||
def error(self, failure):
|
||||
self.disp(
|
||||
_("Error while trying to upload tickets: {reason}").format(reason=failure),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
if self.args.location is None:
|
||||
# no location, the list of importer or description is requested
|
||||
for name in ("option", "service", "node"):
|
||||
if getattr(self.args, name):
|
||||
self.parser.error(
|
||||
_(
|
||||
"{name} argument can't be used without location argument"
|
||||
).format(name=name)
|
||||
)
|
||||
_(f"{name} argument can't be used without location argument"))
|
||||
if self.args.importer is None:
|
||||
self.disp(
|
||||
"\n".join(
|
||||
[
|
||||
"{}: {}".format(name, desc)
|
||||
for name, desc in self.host.bridge.ticketsImportList()
|
||||
f"{name}: {desc}"
|
||||
for name, desc in await self.host.bridge.ticketsImportList()
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
short_desc, long_desc = self.host.bridge.ticketsImportDesc(
|
||||
short_desc, long_desc = await self.host.bridge.ticketsImportDesc(
|
||||
self.args.importer
|
||||
)
|
||||
except Exception as e:
|
||||
msg = [l for l in str(e).split("\n") if l][
|
||||
-1
|
||||
] # we only keep the last line
|
||||
self.disp(msg)
|
||||
self.host.quit(1)
|
||||
self.disp(f"can't get importer description: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
self.disp(
|
||||
"{name}: {short_desc}\n\n{long_desc}".format(
|
||||
name=self.args.importer,
|
||||
short_desc=short_desc,
|
||||
long_desc=long_desc,
|
||||
)
|
||||
)
|
||||
self.disp(f"{name}: {short_desc}\n\n{long_desc}")
|
||||
self.host.quit()
|
||||
else:
|
||||
# we have a location, an import is requested
|
||||
|
||||
if self.args.progress:
|
||||
# we use a custom progress bar template as we want a counter
|
||||
self.pbar_template = [
|
||||
_("Progress: "), ["Percentage"], " ", ["Bar"], " ",
|
||||
["Counter"], " ", ["ETA"]
|
||||
]
|
||||
|
||||
options = {key: value for key, value in self.args.option}
|
||||
fields_map = dict(self.args.map)
|
||||
if fields_map:
|
||||
if FIELDS_MAP in options:
|
||||
self.parser.error(
|
||||
_(
|
||||
"fields_map must be specified either preencoded in --option or using --map, but not both at the same time"
|
||||
)
|
||||
_("fields_map must be specified either preencoded in --option or "
|
||||
"using --map, but not both at the same time")
|
||||
)
|
||||
options[FIELDS_MAP] = json.dumps(fields_map)
|
||||
|
||||
def gotId(id_):
|
||||
self.progress_id = id_
|
||||
|
||||
self.host.bridge.ticketsImport(
|
||||
self.args.importer,
|
||||
self.args.location,
|
||||
options,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
callback=gotId,
|
||||
errback=self.error,
|
||||
)
|
||||
try:
|
||||
progress_id = await self.host.bridge.ticketsImport(
|
||||
self.args.importer,
|
||||
self.args.location,
|
||||
options,
|
||||
self.args.service,
|
||||
self.args.node,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(
|
||||
_(f"Error while trying to import tickets: {e}"),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self.set_progress_id(progress_id)
|
||||
|
||||
|
||||
class Ticket(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ class Parse(base.CommandBase):
|
|||
"uri", help=_("XMPP URI to parse")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self.output(uri.parseXMPPUri(self.args.uri))
|
||||
async def start(self):
|
||||
await self.output(uri.parseXMPPUri(self.args.uri))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Build(base.CommandBase):
|
||||
|
|
@ -65,9 +66,10 @@ class Build(base.CommandBase):
|
|||
help=_("URI fields"),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
async def start(self):
|
||||
fields = dict(self.args.fields) if self.args.fields else {}
|
||||
self.disp(uri.buildXMPPUri(self.args.type, path=self.args.path, **fields))
|
||||
self.host.quit()
|
||||
|
||||
|
||||
class Uri(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@
|
|||
# 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 json
|
||||
import os
|
||||
import os.path
|
||||
import time
|
||||
import tempfile
|
||||
import asyncio
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from sat.core.i18n import _
|
||||
from sat.core import exceptions
|
||||
|
|
@ -26,15 +34,6 @@ from sat.tools.common import uri as xmpp_uri
|
|||
from sat.tools import config
|
||||
from configparser import NoSectionError, NoOptionError
|
||||
from collections import namedtuple
|
||||
from functools import partial
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import time
|
||||
import tempfile
|
||||
import subprocess
|
||||
import glob
|
||||
import shlex
|
||||
|
||||
# defaut arguments used for some known editors (editing with metadata)
|
||||
VIM_SPLIT_ARGS = "-c 'set nospr|vsplit|wincmd w|next|wincmd w'"
|
||||
|
|
@ -76,18 +75,18 @@ def getTmpDir(sat_conf, cat_dir, sub_dir=None):
|
|||
"""Return directory used to store temporary files
|
||||
|
||||
@param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
|
||||
@param cat_dir(unicode): directory of the category (e.g. "blog")
|
||||
@param cat_dir(str): directory of the category (e.g. "blog")
|
||||
@param sub_dir(str): sub directory where data need to be put
|
||||
profile can be used here, or special directory name
|
||||
sub_dir will be escaped to be usable in path (use regex.pathUnescape to find
|
||||
initial str)
|
||||
@return (str): path to the dir
|
||||
@return (Path): path to the dir
|
||||
"""
|
||||
local_dir = config.getConfig(sat_conf, "", "local_dir", Exception)
|
||||
path = [local_dir.encode("utf-8"), cat_dir.encode("utf-8")]
|
||||
path_elts = [local_dir, cat_dir]
|
||||
if sub_dir is not None:
|
||||
path.append(regex.pathEscape(sub_dir))
|
||||
return os.path.join(*path)
|
||||
path_elts.append(regex.pathEscape(sub_dir))
|
||||
return Path(*path_elts)
|
||||
|
||||
|
||||
def parse_args(host, cmd_line, **format_kw):
|
||||
|
|
@ -121,12 +120,14 @@ class BaseEdit(object):
|
|||
@param cat_dir(unicode): directory to use for drafts
|
||||
this will be a sub-directory of SàT's local_dir
|
||||
@param use_metadata(bool): True is edition need a second file for metadata
|
||||
most of signature change with use_metadata with an additional metadata argument.
|
||||
This is done to raise error if a command needs metadata but forget the flag, and vice versa
|
||||
most of signature change with use_metadata with an additional metadata
|
||||
argument.
|
||||
This is done to raise error if a command needs metadata but forget the flag,
|
||||
and vice versa
|
||||
"""
|
||||
self.host = host
|
||||
self.sat_conf = config.parseMainConf()
|
||||
self.cat_dir_str = cat_dir.encode("utf-8")
|
||||
self.cat_dir = cat_dir
|
||||
self.use_metadata = use_metadata
|
||||
|
||||
def secureUnlink(self, path):
|
||||
|
|
@ -135,21 +136,21 @@ class BaseEdit(object):
|
|||
This method is used to prevent accidental deletion of a draft
|
||||
If there are more file in SECURE_UNLINK_DIR than SECURE_UNLINK_MAX,
|
||||
older file are deleted
|
||||
@param path(str): file to unlink
|
||||
@param path(Path, str): file to unlink
|
||||
"""
|
||||
if not os.path.isfile(path):
|
||||
path = Path(path).resolve()
|
||||
if not path.is_file:
|
||||
raise OSError("path must link to a regular file")
|
||||
if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)):
|
||||
if path.parent != getTmpDir(self.sat_conf, self.cat_dir):
|
||||
self.disp(
|
||||
"File {} is not in SàT temporary hierarchy, we do not remove it".format(
|
||||
path
|
||||
),
|
||||
f"File {path} is not in SàT temporary hierarchy, we do not remove "
|
||||
f"it",
|
||||
2,
|
||||
)
|
||||
return
|
||||
# we have 2 files per draft with use_metadata, so we double max
|
||||
unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX
|
||||
backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR)
|
||||
backup_dir = getTmpDir(self.sat_conf, self.cat_dir, SECURE_UNLINK_DIR)
|
||||
if not os.path.exists(backup_dir):
|
||||
os.makedirs(backup_dir)
|
||||
filename = os.path.basename(path)
|
||||
|
|
@ -170,21 +171,15 @@ class BaseEdit(object):
|
|||
self.host.disp("Purging backup file {}".format(path), 2)
|
||||
os.unlink(path)
|
||||
|
||||
def runEditor(
|
||||
self,
|
||||
editor_args_opt,
|
||||
content_file_path,
|
||||
content_file_obj,
|
||||
meta_file_path=None,
|
||||
meta_ori=None,
|
||||
):
|
||||
"""run editor to edit content and metadata
|
||||
async def runEditor(self, editor_args_opt, content_file_path, content_file_obj,
|
||||
meta_file_path=None, meta_ori=None):
|
||||
"""Run editor to edit content and metadata
|
||||
|
||||
@param editor_args_opt(unicode): option in [jp] section in configuration for
|
||||
specific args
|
||||
@param content_file_path(str): path to the content file
|
||||
@param content_file_obj(file): opened file instance
|
||||
@param meta_file_path(str, None): metadata file path
|
||||
@param meta_file_path(str, Path, None): metadata file path
|
||||
if None metadata will not be used
|
||||
@param meta_ori(dict, None): original cotent of metadata
|
||||
can't be used if use_metadata is False
|
||||
|
|
@ -223,26 +218,27 @@ class BaseEdit(object):
|
|||
args = [content_file_path]
|
||||
|
||||
# actual editing
|
||||
editor_exit = subprocess.call([editor] + args)
|
||||
editor_process = await asyncio.create_subprocess_exec(
|
||||
editor, *[str(a) for a in args])
|
||||
editor_exit = await editor_process.wait()
|
||||
|
||||
# edition will now be checked, and data will be sent if it was a success
|
||||
if editor_exit != 0:
|
||||
self.disp(
|
||||
"Editor exited with an error code, so temporary file has not be deleted, and item is not published.\nYou can find temporary file at {path}".format(
|
||||
path=content_file_path
|
||||
),
|
||||
f"Editor exited with an error code, so temporary file has not be "
|
||||
f"deleted, and item is not published.\nYou can find temporary file "
|
||||
f"at {content_file_path}",
|
||||
error=True,
|
||||
)
|
||||
else:
|
||||
# main content
|
||||
try:
|
||||
with open(content_file_path, "rb") as f:
|
||||
with content_file_path.open("rb") as f:
|
||||
content = f.read()
|
||||
except (OSError, IOError):
|
||||
self.disp(
|
||||
"Can read file at {content_path}, have it been deleted?\nCancelling edition".format(
|
||||
content_path=content_file_path
|
||||
),
|
||||
f"Can read file at {content_file_path}, have it been deleted?\n"
|
||||
f"Cancelling edition",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
|
|
@ -250,50 +246,46 @@ class BaseEdit(object):
|
|||
# metadata
|
||||
if self.use_metadata:
|
||||
try:
|
||||
with open(meta_file_path, "rb") as f:
|
||||
with meta_file_path.open("rb") as f:
|
||||
metadata = json.load(f)
|
||||
except (OSError, IOError):
|
||||
self.disp(
|
||||
"Can read file at {meta_file_path}, have it been deleted?\nCancelling edition".format(
|
||||
content_path=content_file_path, meta_path=meta_file_path
|
||||
),
|
||||
f"Can read file at {meta_file_path}, have it been deleted?\n"
|
||||
f"Cancelling edition",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
except ValueError:
|
||||
self.disp(
|
||||
"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n"
|
||||
+ "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format(
|
||||
content_path=content_file_path, meta_path=meta_file_path
|
||||
),
|
||||
f"Can't parse metadata, please check it is correct JSON format. "
|
||||
f"Cancelling edition.\nYou can find tmp file at "
|
||||
f"{content_file_path} and temporary meta file at "
|
||||
f"{meta_file_path}.",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_DATA_ERROR)
|
||||
|
||||
if self.use_metadata and not metadata.get("publish", True):
|
||||
self.disp(
|
||||
'Publication blocked by "publish" key in metadata, cancelling edition.\n\n'
|
||||
+ "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format(
|
||||
content_path=content_file_path, meta_path=meta_file_path
|
||||
),
|
||||
f'Publication blocked by "publish" key in metadata, cancelling '
|
||||
f'edition.\n\ntemporary file path:\t{content_file_path}\nmetadata '
|
||||
f'file path:\t{meta_file_path}',
|
||||
error=True,
|
||||
)
|
||||
self.host.quit()
|
||||
|
||||
if len(content) == 0:
|
||||
self.disp("Content is empty, cancelling the edition")
|
||||
if not content_file_path.startswith(
|
||||
getTmpDir(self.sat_conf, self.cat_dir_str)
|
||||
):
|
||||
if content_file_path.parent != getTmpDir(self.sat_conf, self.cat_dir):
|
||||
self.disp(
|
||||
"File are not in SàT temporary hierarchy, we do not remove them",
|
||||
2,
|
||||
)
|
||||
self.host.quit()
|
||||
self.disp("Deletion of {}".format(content_file_path), 2)
|
||||
self.disp(f"Deletion of {content_file_path}", 2)
|
||||
os.unlink(content_file_path)
|
||||
if self.use_metadata:
|
||||
self.disp("Deletion of {}".format(meta_file_path), 2)
|
||||
self.disp(f"Deletion of {meta_file_path}".format(meta_file_path), 2)
|
||||
os.unlink(meta_file_path)
|
||||
self.host.quit()
|
||||
|
||||
|
|
@ -309,24 +301,21 @@ class BaseEdit(object):
|
|||
content = content.decode("utf-8-sig") # we use utf-8-sig to avoid BOM
|
||||
try:
|
||||
if self.use_metadata:
|
||||
self.publish(content, metadata)
|
||||
await self.publish(content, metadata)
|
||||
else:
|
||||
self.publish(content)
|
||||
await self.publish(content)
|
||||
except Exception as e:
|
||||
if self.use_metadata:
|
||||
self.disp(
|
||||
"Error while sending your item, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format(
|
||||
content_path=content_file_path,
|
||||
meta_path=meta_file_path,
|
||||
reason=e,
|
||||
),
|
||||
f"Error while sending your item, the temporary files have "
|
||||
f"been kept at {content_file_path} and {meta_file_path}: "
|
||||
f"{e}",
|
||||
error=True,
|
||||
)
|
||||
else:
|
||||
self.disp(
|
||||
"Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format(
|
||||
content_path=content_file_path, reason=e
|
||||
),
|
||||
f"Error while sending your item, the temporary file has been "
|
||||
f"kept at {content_file_path}: {e}",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
|
|
@ -335,41 +324,38 @@ class BaseEdit(object):
|
|||
if self.use_metadata:
|
||||
self.secureUnlink(meta_file_path)
|
||||
|
||||
def publish(self, content):
|
||||
async def publish(self, content):
|
||||
# if metadata is needed, publish will be called with it last argument
|
||||
raise NotImplementedError
|
||||
|
||||
def getTmpFile(self):
|
||||
"""Create a temporary file
|
||||
|
||||
@param suff (str): suffix to use for the filename
|
||||
@return (tuple(file, str)): opened (w+b) file object and file path
|
||||
@return (tuple(file, Path)): opened (w+b) file object and file path
|
||||
"""
|
||||
suff = "." + self.getTmpSuff()
|
||||
cat_dir_str = self.cat_dir_str
|
||||
tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode("utf-8"))
|
||||
if not os.path.exists(tmp_dir):
|
||||
cat_dir_str = self.cat_dir
|
||||
tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, self.profile)
|
||||
if not tmp_dir.exists():
|
||||
try:
|
||||
os.makedirs(tmp_dir)
|
||||
tmp_dir.mkdir(parents=True)
|
||||
except OSError as e:
|
||||
self.disp(
|
||||
"Can't create {path} directory: {reason}".format(
|
||||
path=tmp_dir, reason=e
|
||||
),
|
||||
f"Can't create {tmp_dir} directory: {e}",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
try:
|
||||
fd, path = tempfile.mkstemp(
|
||||
suffix=suff.encode("utf-8"),
|
||||
suffix=suff,
|
||||
prefix=time.strftime(cat_dir_str + "_%Y-%m-%d_%H:%M:%S_"),
|
||||
dir=tmp_dir,
|
||||
text=True,
|
||||
)
|
||||
return os.fdopen(fd, "w+b"), path
|
||||
return os.fdopen(fd, "w+b"), Path(path)
|
||||
except OSError as e:
|
||||
self.disp(
|
||||
"Can't create temporary file: {reason}".format(reason=e), error=True
|
||||
f"Can't create temporary file: {e}", error=True
|
||||
)
|
||||
self.host.quit(1)
|
||||
|
||||
|
|
@ -377,27 +363,26 @@ class BaseEdit(object):
|
|||
"""Get most recently edited file
|
||||
|
||||
@param profile(unicode): profile linked to the draft
|
||||
@return(str): full path of current file
|
||||
@return(Path): full path of current file
|
||||
"""
|
||||
# we guess the item currently edited by choosing
|
||||
# the most recent file corresponding to temp file pattern
|
||||
# in tmp_dir, excluding metadata files
|
||||
cat_dir_str = self.cat_dir_str
|
||||
tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode("utf-8"))
|
||||
tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, profile)
|
||||
available = [
|
||||
path
|
||||
for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + "_*"))
|
||||
if not path.endswith(METADATA_SUFF)
|
||||
p
|
||||
for p in tmp_dir.glob(f'{self.cat_dir}_*')
|
||||
if not p.match(f"*{METADATA_SUFF}")
|
||||
]
|
||||
if not available:
|
||||
self.disp(
|
||||
"Could not find any content draft in {path}".format(path=tmp_dir),
|
||||
f"Could not find any content draft in {tmp_dir}",
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(1)
|
||||
return max(available, key=lambda path: os.stat(path).st_mtime)
|
||||
return max(available, key=lambda p: p.stat().st_mtime)
|
||||
|
||||
def getItemData(self, service, node, item):
|
||||
async def getItemData(self, service, node, item):
|
||||
"""return formatted content, metadata (or not if use_metadata is false), and item id"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -405,8 +390,8 @@ class BaseEdit(object):
|
|||
"""return suffix used for content file"""
|
||||
return "xml"
|
||||
|
||||
def getItemPath(self):
|
||||
"""retrieve item path (i.e. service and node) from item argument
|
||||
async def getItemPath(self):
|
||||
"""Retrieve item path (i.e. service and node) from item argument
|
||||
|
||||
This method is obviously only useful for edition of PubSub based features
|
||||
"""
|
||||
|
|
@ -419,15 +404,15 @@ class BaseEdit(object):
|
|||
# user wants to continue current draft
|
||||
content_file_path = self.getCurrentFile(self.profile)
|
||||
self.disp("Continuing edition of current draft", 2)
|
||||
content_file_obj = open(content_file_path, "r+b")
|
||||
content_file_obj = content_file_path.open("r+b")
|
||||
# we seek at the end of file in case of an item already exist
|
||||
# this will write content of the existing item at the end of the draft.
|
||||
# This way no data should be lost.
|
||||
content_file_obj.seek(0, os.SEEK_END)
|
||||
elif self.args.draft_path:
|
||||
# there is an existing draft that we use
|
||||
content_file_path = os.path.expanduser(self.args.draft_path)
|
||||
content_file_obj = open(content_file_path, "r+b")
|
||||
content_file_path = self.args.draft_path.expanduser()
|
||||
content_file_obj = content_file_path.open("r+b")
|
||||
# we seek at the end for the same reason as above
|
||||
content_file_obj.seek(0, os.SEEK_END)
|
||||
else:
|
||||
|
|
@ -438,9 +423,9 @@ class BaseEdit(object):
|
|||
self.disp("Editing requested published item", 2)
|
||||
try:
|
||||
if self.use_metadata:
|
||||
content, metadata, item = self.getItemData(service, node, item)
|
||||
content, metadata, item = await self.getItemData(service, node, item)
|
||||
else:
|
||||
content, item = self.getItemData(service, node, item)
|
||||
content, item = await self.getItemData(service, node, item)
|
||||
except Exception as e:
|
||||
# FIXME: ugly but we have not good may to check errors in bridge
|
||||
if "item-not-found" in str(e):
|
||||
|
|
@ -451,13 +436,14 @@ class BaseEdit(object):
|
|||
else:
|
||||
self.disp(
|
||||
_(
|
||||
'item "{item_id}" not found, we create a new item with this id'
|
||||
).format(item_id=item),
|
||||
f'item "{item}" not found, we create a new item with'
|
||||
f'this id'
|
||||
),
|
||||
2,
|
||||
)
|
||||
content_file_obj.seek(0)
|
||||
else:
|
||||
self.disp("Error while retrieving item: {}".format(e))
|
||||
self.disp(f"Error while retrieving item: {e}")
|
||||
self.host.quit(C.EXIT_ERROR)
|
||||
else:
|
||||
# item exists, we write content
|
||||
|
|
@ -468,7 +454,7 @@ class BaseEdit(object):
|
|||
content_file_obj.write(content.encode("utf-8"))
|
||||
content_file_obj.seek(0)
|
||||
self.disp(
|
||||
_('item "{item_id}" found, we edit it').format(item_id=item), 2
|
||||
_(f'item "{item}" found, we edit it'), 2
|
||||
)
|
||||
else:
|
||||
self.disp("Editing a new item", 2)
|
||||
|
|
@ -757,89 +743,70 @@ class Table(object):
|
|||
return self.display(**kwargs_)
|
||||
|
||||
|
||||
class URIFinder(object):
|
||||
"""Helper class to find URIs in well-known locations"""
|
||||
async def fill_well_known_uri(command, path, key, meta_map=None):
|
||||
"""Look for URIs in well-known location and fill appropriate args if suitable
|
||||
|
||||
def __init__(self, command, path, key, callback, meta_map=None):
|
||||
"""
|
||||
@param command(CommandBase): command instance
|
||||
args of this instance will be updated with found values
|
||||
@param path(unicode): absolute path to use as a starting point to look for URIs
|
||||
@param key(unicode): key to look for
|
||||
@param callback(callable): method to call once URIs are found (or not)
|
||||
@param meta_map(dict, None): if not None, map metadata to arg name
|
||||
key is metadata used attribute name
|
||||
value is name to actually use, or None to ignore
|
||||
use empty dict to only retrieve URI
|
||||
possible keys are currently:
|
||||
- labels
|
||||
"""
|
||||
if not command.args.service and not command.args.node:
|
||||
self.host = command.host
|
||||
self.args = command.args
|
||||
self.key = key
|
||||
self.callback = callback
|
||||
self.meta_map = meta_map
|
||||
self.host.bridge.URIFind(
|
||||
path,
|
||||
[key],
|
||||
callback=self.URIFindCb,
|
||||
errback=partial(
|
||||
command.errback,
|
||||
msg=_("can't find " + key + " URI: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
else:
|
||||
callback()
|
||||
@param command(CommandBase): command instance
|
||||
args of this instance will be updated with found values
|
||||
@param path(unicode): absolute path to use as a starting point to look for URIs
|
||||
@param key(unicode): key to look for
|
||||
@param meta_map(dict, None): if not None, map metadata to arg name
|
||||
key is metadata used attribute name
|
||||
value is name to actually use, or None to ignore
|
||||
use empty dict to only retrieve URI
|
||||
possible keys are currently:
|
||||
- labels
|
||||
"""
|
||||
args = command.args
|
||||
if args.service or args.node:
|
||||
# we only look for URIs if a service and a node are not already specified
|
||||
return
|
||||
|
||||
def setMetadataList(self, uri_data, key):
|
||||
"""Helper method to set list of values from metadata
|
||||
host = command.host
|
||||
|
||||
@param uri_data(dict): data of the found URI
|
||||
@param key(unicode): key of the value to retrieve
|
||||
"""
|
||||
new_values_json = uri_data.get(key)
|
||||
try:
|
||||
uris_data = await host.bridge.URIFind(path, [key])
|
||||
except Exception as e:
|
||||
host.disp(f"can't find {key} URI: {e}", error=True)
|
||||
host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
|
||||
try:
|
||||
uri_data = uris_data[key]
|
||||
except KeyError:
|
||||
host.disp(
|
||||
_(f"No {key} URI specified for this project, please specify service and "
|
||||
f"node"),
|
||||
error=True,
|
||||
)
|
||||
host.quit(C.EXIT_NOT_FOUND)
|
||||
|
||||
uri = uri_data["uri"]
|
||||
|
||||
# set extra metadata if they are specified
|
||||
for data_key in ['labels']:
|
||||
new_values_json = uri_data.get(data_key)
|
||||
if uri_data is not None:
|
||||
if self.meta_map is None:
|
||||
dest = key
|
||||
if meta_map is None:
|
||||
dest = data_key
|
||||
else:
|
||||
dest = self.meta_map.get(key)
|
||||
dest = meta_map.get(data_key)
|
||||
if dest is None:
|
||||
return
|
||||
continue
|
||||
|
||||
try:
|
||||
values = getattr(self.args, key)
|
||||
values = getattr(args, data_key)
|
||||
except AttributeError:
|
||||
raise exceptions.InternalError(
|
||||
'there is no "{key}" arguments'.format(key=key)
|
||||
)
|
||||
raise exceptions.InternalError(f'there is no {data_key!r} arguments')
|
||||
else:
|
||||
if values is None:
|
||||
values = []
|
||||
values.extend(json.loads(new_values_json))
|
||||
setattr(self.args, dest, values)
|
||||
setattr(args, dest, values)
|
||||
|
||||
def URIFindCb(self, uris_data):
|
||||
try:
|
||||
uri_data = uris_data[self.key]
|
||||
except KeyError:
|
||||
self.host.disp(
|
||||
_(
|
||||
"No {key} URI specified for this project, please specify service and node"
|
||||
).format(key=self.key),
|
||||
error=True,
|
||||
)
|
||||
self.host.quit(C.EXIT_NOT_FOUND)
|
||||
else:
|
||||
uri = uri_data["uri"]
|
||||
|
||||
self.setMetadataList(uri_data, "labels")
|
||||
parsed_uri = xmpp_uri.parseXMPPUri(uri)
|
||||
try:
|
||||
self.args.service = parsed_uri["path"]
|
||||
self.args.node = parsed_uri["node"]
|
||||
except KeyError:
|
||||
self.host.disp(_("Invalid URI found: {uri}").format(uri=uri), error=True)
|
||||
self.host.quit(C.EXIT_DATA_ERROR)
|
||||
self.callback()
|
||||
parsed_uri = xmpp_uri.parseXMPPUri(uri)
|
||||
try:
|
||||
args.service = parsed_uri["path"]
|
||||
args.node = parsed_uri["node"]
|
||||
except KeyError:
|
||||
host.disp(_(f"Invalid URI found: {uri}"), error=True)
|
||||
host.quit(C.EXIT_DATA_ERROR)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class Const(constants.Const):
|
|||
A_LEVEL_COLORS = (A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN)
|
||||
A_SUCCESS = A.BOLD + A.FG_GREEN
|
||||
A_FAILURE = A.BOLD + A.FG_RED
|
||||
A_WARNING = A.BOLD + A.FG_RED
|
||||
# A_PROMPT_* is for shell
|
||||
A_PROMPT_PATH = A.BOLD + A.FG_CYAN
|
||||
A_PROMPT_SUF = A.BOLD
|
||||
|
|
@ -83,6 +84,7 @@ class Const(constants.Const):
|
|||
EXIT_DATA_ERROR = 17 # data needed for a command is invalid
|
||||
EXIT_MISSING_FEATURE = 18 # a needed plugin or feature is not available
|
||||
EXIT_USER_CANCELLED = 20 # user cancelled action
|
||||
EXIT_INTERNAL_ERROR = 111 # unexpected error
|
||||
EXIT_FILE_NOT_EXE = (
|
||||
126
|
||||
) # a file to be executed was found, but it was not an executable utility (cf. man 1 exit)
|
||||
|
|
|
|||
|
|
@ -21,5 +21,4 @@ from sat_frontends.jp import base
|
|||
|
||||
if __name__ == "__main__":
|
||||
jp = base.Jp()
|
||||
jp.import_plugins()
|
||||
jp.run()
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ class XMLUI(object):
|
|||
C.OUTPUT_LIST_XMLUI, "simple", self.xmlui_list, default=True
|
||||
)
|
||||
|
||||
def xmlui(self, data):
|
||||
async def xmlui(self, data):
|
||||
xmlui = xmlui_manager.create(self.host, data)
|
||||
xmlui.show(values_only=True, read_only=True)
|
||||
await xmlui.show(values_only=True, read_only=True)
|
||||
self.host.disp("")
|
||||
|
||||
def xmlui_list(self, data):
|
||||
async def xmlui_list(self, data):
|
||||
for d in data:
|
||||
self.xmlui(d)
|
||||
await self.xmlui(d)
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@
|
|||
# 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 functools import partial
|
||||
from sat.core.log import getLogger
|
||||
|
||||
log = getLogger(__name__)
|
||||
from sat_frontends.tools import xmlui as xmlui_base
|
||||
from sat_frontends.jp.constants import Const as C
|
||||
from sat.tools.common.ansi import ANSI as A
|
||||
from sat.core.i18n import _
|
||||
from functools import partial
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
# workflow constants
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ class Widget(Base):
|
|||
def name(self):
|
||||
return self._xmlui_name
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
"""display current widget
|
||||
|
||||
must be overriden by subclasses
|
||||
|
|
@ -178,14 +178,14 @@ class EmptyWidget(xmlui_base.EmptyWidget, Widget):
|
|||
def __init__(self, xmlui_parent):
|
||||
Widget.__init__(self, xmlui_parent)
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
self.host.disp('')
|
||||
|
||||
|
||||
class TextWidget(xmlui_base.TextWidget, ValueWidget):
|
||||
type = "text"
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
self.host.disp(self.value)
|
||||
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ class LabelWidget(xmlui_base.LabelWidget, ValueWidget):
|
|||
except AttributeError:
|
||||
return None
|
||||
|
||||
def show(self, no_lf=False, ansi=""):
|
||||
async def show(self, no_lf=False, ansi=""):
|
||||
"""show label
|
||||
|
||||
@param no_lf(bool): same as for [JP.disp]
|
||||
|
|
@ -214,16 +214,16 @@ class JidWidget(xmlui_base.JidWidget, TextWidget):
|
|||
class StringWidget(xmlui_base.StringWidget, InputWidget):
|
||||
type = "string"
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
if self.read_only or self.root.read_only:
|
||||
self.disp(self.value)
|
||||
else:
|
||||
elems = []
|
||||
self.verboseName(elems)
|
||||
if self.value:
|
||||
elems.append(_("(enter: {default})").format(default=self.value))
|
||||
elems.append(_(f"(enter: {self.value})"))
|
||||
elems.extend([C.A_HEADER, "> "])
|
||||
value = input(A.color(*elems).encode('utf-8'))
|
||||
value = await self.host.ainput(A.color(*elems))
|
||||
if value:
|
||||
# TODO: empty value should be possible
|
||||
# an escape key should be used for default instead of enter with empty value
|
||||
|
|
@ -238,7 +238,7 @@ class TextBoxWidget(xmlui_base.TextWidget, StringWidget):
|
|||
type = "textbox"
|
||||
# TODO: use a more advanced input method
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
self.verboseName()
|
||||
if self.read_only or self.root.read_only:
|
||||
self.disp(self.value)
|
||||
|
|
@ -251,9 +251,9 @@ class TextBoxWidget(xmlui_base.TextWidget, StringWidget):
|
|||
while True:
|
||||
try:
|
||||
if not values:
|
||||
line = input(A.color(C.A_HEADER, "[Ctrl-D to finish]> "))
|
||||
line = await self.host.ainput(A.color(C.A_HEADER, "[Ctrl-D to finish]> "))
|
||||
else:
|
||||
line = input()
|
||||
line = await self.host.ainput()
|
||||
values.append(line)
|
||||
except EOFError:
|
||||
break
|
||||
|
|
@ -264,20 +264,20 @@ class TextBoxWidget(xmlui_base.TextWidget, StringWidget):
|
|||
class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget):
|
||||
type = "xhtmlbox"
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
# FIXME: we use bridge in a blocking way as permitted by python-dbus
|
||||
# this only for now to make it simpler, it must be refactored
|
||||
# to use async when jp will be fully async (expected for 0.8)
|
||||
self.value = self.host.bridge.syntaxConvert(
|
||||
self.value = await self.host.bridge.syntaxConvert(
|
||||
self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile)
|
||||
super(XHTMLBoxWidget, self).show()
|
||||
await super(XHTMLBoxWidget, self).show()
|
||||
|
||||
|
||||
class ListWidget(xmlui_base.ListWidget, OptionsWidget):
|
||||
type = "list"
|
||||
# TODO: handle flags, notably multi
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
if self.root.values_only:
|
||||
for value in self.values:
|
||||
self.disp(self.value)
|
||||
|
|
@ -308,8 +308,8 @@ class ListWidget(xmlui_base.ListWidget, OptionsWidget):
|
|||
choice = None
|
||||
limit_max = len(self.options) - 1
|
||||
while choice is None or choice < 0 or choice > limit_max:
|
||||
choice = input(
|
||||
A.color(C.A_HEADER, _("your choice (0-{max}): ").format(max=limit_max))
|
||||
choice = await self.host.ainput(
|
||||
A.color(C.A_HEADER, _(f"your choice (0-{limit_max}): "))
|
||||
)
|
||||
try:
|
||||
choice = int(choice)
|
||||
|
|
@ -322,7 +322,7 @@ class ListWidget(xmlui_base.ListWidget, OptionsWidget):
|
|||
class BoolWidget(xmlui_base.BoolWidget, InputWidget):
|
||||
type = "bool"
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
disp_true = A.color(A.FG_GREEN, "TRUE")
|
||||
disp_false = A.color(A.FG_RED, "FALSE")
|
||||
if self.read_only or self.root.read_only:
|
||||
|
|
@ -338,7 +338,7 @@ class BoolWidget(xmlui_base.BoolWidget, InputWidget):
|
|||
while choice not in ("0", "1"):
|
||||
elems = [C.A_HEADER, _("your choice (0,1): ")]
|
||||
self.verboseName(elems)
|
||||
choice = input(A.color(*elems))
|
||||
choice = await self.host.ainput(A.color(*elems))
|
||||
self.value = bool(int(choice))
|
||||
self.disp("")
|
||||
|
||||
|
|
@ -365,9 +365,9 @@ class Container(Base):
|
|||
def _xmluiRemove(self, widget):
|
||||
self.children.remove(widget)
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
for child in self.children:
|
||||
child.show()
|
||||
await child.show()
|
||||
|
||||
|
||||
class VerticalContainer(xmlui_base.VerticalContainer, Container):
|
||||
|
|
@ -381,7 +381,7 @@ class PairsContainer(xmlui_base.PairsContainer, Container):
|
|||
class LabelContainer(xmlui_base.PairsContainer, Container):
|
||||
type = "label"
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
for child in self.children:
|
||||
no_lf = False
|
||||
# we check linked widget type
|
||||
|
|
@ -399,9 +399,9 @@ class LabelContainer(xmlui_base.PairsContainer, Container):
|
|||
no_lf = True
|
||||
elif wid_type == "bool" and for_widget.read_only:
|
||||
no_lf = True
|
||||
child.show(no_lf=no_lf, ansi=A.FG_CYAN)
|
||||
await child.show(no_lf=no_lf, ansi=A.FG_CYAN)
|
||||
else:
|
||||
child.show()
|
||||
await child.show()
|
||||
|
||||
|
||||
## Dialogs ##
|
||||
|
|
@ -415,24 +415,61 @@ class Dialog(object):
|
|||
def disp(self, *args, **kwargs):
|
||||
self.host.disp(*args, **kwargs)
|
||||
|
||||
def show(self):
|
||||
async def show(self):
|
||||
"""display current dialog
|
||||
|
||||
must be overriden by subclasses
|
||||
"""
|
||||
raise NotImplementedError(self.__class__)
|
||||
|
||||
class MessageDialog(xmlui_base.MessageDialog, Dialog):
|
||||
|
||||
def __init__(self, xmlui_parent, title, message, level):
|
||||
Dialog.__init__(self, xmlui_parent)
|
||||
xmlui_base.MessageDialog.__init__(self, xmlui_parent)
|
||||
self.title, self.message, self.level = title, message, level
|
||||
|
||||
async def show(self):
|
||||
# TODO: handle level
|
||||
if self.title:
|
||||
self.disp(A.color(C.A_HEADER, self.title))
|
||||
self.disp(self.message)
|
||||
|
||||
|
||||
class NoteDialog(xmlui_base.NoteDialog, Dialog):
|
||||
def show(self):
|
||||
# TODO: handle title and level
|
||||
self.disp(self.message)
|
||||
|
||||
def __init__(self, xmlui_parent, title, message, level):
|
||||
Dialog.__init__(self, xmlui_parent)
|
||||
xmlui_base.NoteDialog.__init__(self, xmlui_parent)
|
||||
self.title, self.message, self.level = title, message, level
|
||||
|
||||
async def show(self):
|
||||
# TODO: handle title and level
|
||||
self.disp(self.message)
|
||||
|
||||
|
||||
class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog):
|
||||
|
||||
def __init__(self, xmlui_parent, title, message, level, buttons_set):
|
||||
Dialog.__init__(self, xmlui_parent)
|
||||
xmlui_base.ConfirmDialog.__init__(self, xmlui_parent)
|
||||
self.title, self.message, self.level, self.buttons_set = (
|
||||
title, message, level, buttons_set)
|
||||
|
||||
async def show(self):
|
||||
# TODO: handle buttons_set and level
|
||||
self.disp(self.message)
|
||||
if self.title:
|
||||
self.disp(A.color(C.A_HEADER, self.title))
|
||||
input_ = None
|
||||
while input_ not in ('y', 'n'):
|
||||
input_ = await self.host.ainput(f"{self.message} (y/n)? ")
|
||||
input_ = input_.lower()
|
||||
if input_ == 'y':
|
||||
self._xmluiValidated()
|
||||
else:
|
||||
self._xmluiCancelled()
|
||||
|
||||
|
||||
## Factory ##
|
||||
|
||||
|
|
@ -444,7 +481,7 @@ class WidgetFactory(object):
|
|||
return cls
|
||||
|
||||
|
||||
class XMLUIPanel(xmlui_base.XMLUIPanel):
|
||||
class XMLUIPanel(xmlui_base.AIOXMLUIPanel):
|
||||
widget_factory = WidgetFactory()
|
||||
_actions = 0 # use to keep track of bridge's launchAction calls
|
||||
read_only = False
|
||||
|
|
@ -470,7 +507,7 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
|
|||
def command(self):
|
||||
return self.host.command
|
||||
|
||||
def show(self, workflow=None, read_only=False, values_only=False):
|
||||
async def show(self, workflow=None, read_only=False, values_only=False):
|
||||
"""display the panel
|
||||
|
||||
@param workflow(list, None): command to execute if not None
|
||||
|
|
@ -489,11 +526,11 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
|
|||
if workflow:
|
||||
XMLUIPanel.workflow = workflow
|
||||
if XMLUIPanel.workflow:
|
||||
self.runWorkflow()
|
||||
await self.runWorkflow()
|
||||
else:
|
||||
self.main_cont.show()
|
||||
await self.main_cont.show()
|
||||
|
||||
def runWorkflow(self):
|
||||
async def runWorkflow(self):
|
||||
"""loop into workflow commands and execute commands
|
||||
|
||||
SUBMIT will interrupt workflow (which will be continue on callback)
|
||||
|
|
@ -506,7 +543,7 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
|
|||
except IndexError:
|
||||
break
|
||||
if cmd == SUBMIT:
|
||||
self.onFormSubmitted()
|
||||
await self.onFormSubmitted()
|
||||
self.submit_id = None # avoid double submit
|
||||
return
|
||||
elif isinstance(cmd, list):
|
||||
|
|
@ -515,32 +552,32 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
|
|||
if widget.type == "bool":
|
||||
value = C.bool(value)
|
||||
widget.value = value
|
||||
self.show()
|
||||
await self.show()
|
||||
|
||||
def submitForm(self, callback=None):
|
||||
async def submitForm(self, callback=None):
|
||||
XMLUIPanel._submit_cb = callback
|
||||
self.onFormSubmitted()
|
||||
await self.onFormSubmitted()
|
||||
|
||||
def onFormSubmitted(self, ignore=None):
|
||||
# self.submitted is a Q&D workaround to avoid
|
||||
async def onFormSubmitted(self, ignore=None):
|
||||
# self.submitted is a Q&D workaround to avoid
|
||||
# double submit when a workflow is set
|
||||
if self.submitted:
|
||||
return
|
||||
self.submitted = True
|
||||
super(XMLUIPanel, self).onFormSubmitted(ignore)
|
||||
await super(XMLUIPanel, self).onFormSubmitted(ignore)
|
||||
|
||||
def _xmluiClose(self):
|
||||
pass
|
||||
|
||||
def _launchActionCb(self, data):
|
||||
async def _launchActionCb(self, data):
|
||||
XMLUIPanel._actions -= 1
|
||||
assert XMLUIPanel._actions >= 0
|
||||
if "xmlui" in data:
|
||||
xmlui_raw = data["xmlui"]
|
||||
xmlui = create(self.host, xmlui_raw)
|
||||
xmlui.show()
|
||||
await xmlui.show()
|
||||
if xmlui.submit_id:
|
||||
xmlui.onFormSubmitted()
|
||||
await xmlui.onFormSubmitted()
|
||||
# TODO: handle data other than XMLUI
|
||||
if not XMLUIPanel._actions:
|
||||
if self._submit_cb is None:
|
||||
|
|
@ -548,19 +585,19 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
|
|||
else:
|
||||
self._submit_cb()
|
||||
|
||||
def _xmluiLaunchAction(self, action_id, data):
|
||||
async def _xmluiLaunchAction(self, action_id, data):
|
||||
XMLUIPanel._actions += 1
|
||||
self.host.bridge.launchAction(
|
||||
action_id,
|
||||
data,
|
||||
self.profile,
|
||||
callback=self._launchActionCb,
|
||||
errback=partial(
|
||||
self.command.errback,
|
||||
msg=_("can't launch XMLUI action: {}"),
|
||||
exit_code=C.EXIT_BRIDGE_ERRBACK,
|
||||
),
|
||||
)
|
||||
try:
|
||||
data = await self.host.bridge.launchAction(
|
||||
action_id,
|
||||
data,
|
||||
self.profile,
|
||||
)
|
||||
except Exception as e:
|
||||
self.disp(f"can't launch XMLUI action: {e}", error=True)
|
||||
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
||||
else:
|
||||
await self._launchActionCb(data)
|
||||
|
||||
|
||||
class XMLUIDialog(xmlui_base.XMLUIDialog):
|
||||
|
|
@ -568,8 +605,8 @@ class XMLUIDialog(xmlui_base.XMLUIDialog):
|
|||
dialog_factory = WidgetFactory()
|
||||
read_only = False
|
||||
|
||||
def show(self, __=None):
|
||||
self.dlg.show()
|
||||
async def show(self, __=None):
|
||||
await self.dlg.show()
|
||||
|
||||
def _xmluiClose(self):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -925,6 +925,67 @@ class XMLUIPanel(XMLUIBase):
|
|||
pass
|
||||
|
||||
|
||||
class AIOXMLUIPanel(XMLUIPanel):
|
||||
"""Asyncio compatible version of XMLUIPanel"""
|
||||
|
||||
async def onFormSubmitted(self, ignore=None):
|
||||
"""An XMLUI form has been submited
|
||||
|
||||
call the submit action associated with this form
|
||||
"""
|
||||
selected_values = []
|
||||
for ctrl_name in self.ctrl_list:
|
||||
escaped = self.escape(ctrl_name)
|
||||
ctrl = self.ctrl_list[ctrl_name]
|
||||
if isinstance(ctrl["control"], ListWidget):
|
||||
selected_values.append(
|
||||
(escaped, "\t".join(ctrl["control"]._xmluiGetSelectedValues()))
|
||||
)
|
||||
else:
|
||||
selected_values.append((escaped, ctrl["control"]._xmluiGetValue()))
|
||||
data = dict(selected_values)
|
||||
for key, value in self.hidden.items():
|
||||
data[self.escape(key)] = value
|
||||
|
||||
if self.submit_id is not None:
|
||||
await self.submit(data)
|
||||
else:
|
||||
log.warning(
|
||||
_("The form data is not sent back, the type is not managed properly")
|
||||
)
|
||||
self._xmluiClose()
|
||||
|
||||
async def onFormCancelled(self, *__):
|
||||
"""Called when a form is cancelled"""
|
||||
log.debug(_("Cancelling form"))
|
||||
if self.submit_id is not None:
|
||||
data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
|
||||
await self.submit(data)
|
||||
else:
|
||||
log.warning(
|
||||
_("The form data is not sent back, the type is not managed properly")
|
||||
)
|
||||
self._xmluiClose()
|
||||
|
||||
async def submit(self, data):
|
||||
self._xmluiClose()
|
||||
if self.submit_id is None:
|
||||
raise ValueError("Can't submit is self.submit_id is not set")
|
||||
if "session_id" in data:
|
||||
raise ValueError(
|
||||
"session_id must no be used in data, it is automaticaly filled with "
|
||||
"self.session_id if present"
|
||||
)
|
||||
if self.session_id is not None:
|
||||
data["session_id"] = self.session_id
|
||||
await self._xmluiLaunchAction(self.submit_id, data)
|
||||
|
||||
async def _xmluiLaunchAction(self, action_id, data):
|
||||
await self.host.launchAction(
|
||||
action_id, data, callback=self.callback, profile=self.profile
|
||||
)
|
||||
|
||||
|
||||
class XMLUIDialog(XMLUIBase):
|
||||
dialog_factory = None
|
||||
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -87,7 +87,6 @@ setup(
|
|||
url="https://salut-a-toi.org",
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
|
|
@ -110,5 +109,5 @@ setup(
|
|||
use_scm_version=sat_dev_version if is_dev_version else False,
|
||||
install_requires=install_requires,
|
||||
package_data={"sat": ["VERSION"]},
|
||||
python_requires=">=3.6",
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SAT: a jabber client
|
||||
|
|
|
|||
Loading…
Reference in a new issue