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:
Goffi 2019-09-25 08:56:41 +02:00
commit 76aadbb3fb
60 changed files with 3299 additions and 3300 deletions

View file

@ -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"

View file

@ -260,7 +260,7 @@ class SAT(service.Service):
)
self._unimport_plugin(plugin_path)
continue
except Exception as e:
except Exception:
import traceback
log.error(

View file

@ -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 ##

View file

@ -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

View file

@ -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))

View file

@ -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]

View file

@ -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")

View file

@ -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,

View file

@ -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):

View file

@ -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():

View file

@ -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

View file

@ -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)

View file

@ -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}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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"):

View file

@ -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

View file

@ -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 "")
)

View file

@ -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(

View file

@ -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"],

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -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__(

View file

@ -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)

View file

@ -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):

View file

@ -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")

View file

@ -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,)

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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'))

View file

@ -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,)

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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()

View file

@ -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):

View file

@ -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

View file

@ -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"))

View file

@ -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)

View file

@ -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):

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -21,5 +21,4 @@ from sat_frontends.jp import base
if __name__ == "__main__":
jp = base.Jp()
jp.import_plugins()
jp.run()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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",
)

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SAT: a jabber client