diff sat_frontends/jp/cmd_roster.py @ 3040:fee60f17ebac

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.
author Goffi <goffi@goffi.org>
date Wed, 25 Sep 2019 08:56:41 +0200
parents ab2696e34d29
children 9d0df638c8b4
line wrap: on
line diff
--- a/sat_frontends/jp/cmd_roster.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_roster.py	Wed Sep 25 08:56:41 2019 +0200
@@ -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"))
-
-    def start(self):
-        self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
+        pass
 
-    def error(self, failure):
-        print((_("Error while retrieving the contacts [%s]") % failure))
-        self.host.quit(1)
+    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 ask_confirmation(self, no_sub, no_from, no_to):
-        """Ask the confirmation before removing contacts.
+            if self.verbosity >= 1:
+                keys_to_show.append('groups')
+                all_keys.remove('groups')
+            if self.verbosity >= 2:
+                keys_to_show.extend(all_keys)
 
-        @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
+            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)))
 
-    def gotContacts(self, contacts):
-        """Process the list of contacts.
+    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)
 
-        @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)
+        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
+
+        await self.output(contacts_dict)
         self.host.quit()
 
 
@@ -98,23 +88,17 @@
 
     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 @@
                     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 @@
         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,53 +145,90 @@
             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)
+        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:
+            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")
 
-    def gotContacts(self, contacts):
-        """Process the list of contacts.
+        self.host.quit()
+
+    async def ask_confirmation(self, no_sub, no_from, no_to):
+        """Ask the confirmation before removing contacts.
 
-        @param contacts(list[tuple]): list of contacts with their attributes and groups
+        @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
         """
-        field_count = 1  # only display the contact by default
-        if self.args.subscriptions:
-            field_count += 3  # ask, from, to
-        if self.args.name:
-            field_count += 1
-        if self.args.groups:
-            field_count += 1
-        for contact, attrs, groups in contacts:
-            args = [contact]
-            if self.args.subscriptions:
-                args.append("ask" if C.bool(attrs["ask"]) else "")
-                args.append("from" if C.bool(attrs["from"]) else "")
-                args.append("to" if C.bool(attrs["to"]) else "")
-            if self.args.name:
-                args.append(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"))
-        self.host.quit()
+        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):
@@ -212,27 +236,24 @@
     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"))