comparison 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
comparison
equal deleted inserted replaced
3039:a1bc34f90fa5 3040:fee60f17ebac
18 # You should have received a copy of the GNU Affero General Public License 18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>. 19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 20
21 from . import base 21 from . import base
22 from collections import OrderedDict 22 from collections import OrderedDict
23 from functools import partial
24 from sat.core.i18n import _ 23 from sat.core.i18n import _
25 from sat_frontends.jp.constants import Const as C 24 from sat_frontends.jp.constants import Const as C
26 from twisted.words.protocols.jabber import jid 25 from sat_frontends.tools import jid
26 from sat.tools.common.ansi import ANSI as A
27 27
28 __commands__ = ["Roster"] 28 __commands__ = ["Roster"]
29 29
30 30
31 31 class Get(base.CommandBase):
32 class Purge(base.CommandBase): 32
33 33 def __init__(self, host):
34 def __init__(self, host): 34 super().__init__(
35 super(Purge, self).__init__(host, 'purge', help=_('Purge the roster from its contacts with no subscription')) 35 host, 'get', use_output=C.OUTPUT_DICT, use_verbose=True,
36 self.need_loop = True 36 extra_outputs = {"default": self.default_output},
37 37 help=_('retrieve the roster entities'))
38 def add_parser_options(self): 38
39 self.parser.add_argument("--no_from", action="store_true", help=_("Also purge contacts with no 'from' subscription")) 39 def add_parser_options(self):
40 self.parser.add_argument("--no_to", action="store_true", help=_("Also purge contacts with no 'to' subscription")) 40 pass
41 41
42 def start(self): 42 def default_output(self, data):
43 self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) 43 for contact_jid, contact_data in data.items():
44 44 all_keys = list(contact_data.keys())
45 def error(self, failure): 45 keys_to_show = []
46 print((_("Error while retrieving the contacts [%s]") % failure)) 46 name = contact_data.get('name', contact_jid.node)
47 self.host.quit(1) 47
48 48 if self.verbosity >= 1:
49 def ask_confirmation(self, no_sub, no_from, no_to): 49 keys_to_show.append('groups')
50 """Ask the confirmation before removing contacts. 50 all_keys.remove('groups')
51 51 if self.verbosity >= 2:
52 @param no_sub (list[unicode]): list of contacts with no subscription 52 keys_to_show.extend(all_keys)
53 @param no_from (list[unicode]): list of contacts with no 'from' subscription 53
54 @param no_to (list[unicode]): list of contacts with no 'to' subscription 54 if name is None:
55 @return bool 55 self.disp(A.color(C.A_HEADER, contact_jid))
56 """ 56 else:
57 if no_sub: 57 self.disp(A.color(C.A_HEADER, name, A.RESET, f" ({contact_jid})"))
58 print("There's no subscription between profile [%s] and the following contacts:" % self.host.profile) 58 for k in keys_to_show:
59 print(" " + "\n ".join(no_sub)) 59 value = contact_data[k]
60 if no_from: 60 if value:
61 print("There's no 'from' subscription between profile [%s] and the following contacts:" % self.host.profile) 61 if isinstance(value, list):
62 print(" " + "\n ".join(no_from)) 62 value = ', '.join(value)
63 if no_to: 63 self.disp(A.color(
64 print("There's no 'to' subscription between profile [%s] and the following contacts:" % self.host.profile) 64 " ", C.A_SUBHEADER, f"{k}: ", A.RESET, str(value)))
65 print(" " + "\n ".join(no_to)) 65
66 message = "REMOVE them from profile [%s]'s roster" % self.host.profile 66 async def start(self):
67 while True: 67 try:
68 res = input("%s (y/N)? " % message) 68 contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
69 if not res or res.lower() == 'n': 69 except Exception as e:
70 return False 70 self.disp(f"error while retrieving the contacts: {e}", error=True)
71 if res.lower() == 'y': 71 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
72 return True 72
73 73 contacts_dict = {}
74 def gotContacts(self, contacts): 74 for contact_jid_s, data, groups in contacts:
75 """Process the list of contacts. 75 # FIXME: we have to convert string to bool here for historical reason
76 76 # getContacts format should be changed and serialised properly
77 @param contacts(list[tuple]): list of contacts with their attributes and groups 77 for key in ('from', 'to', 'ask'):
78 """ 78 if key in data:
79 no_sub, no_from, no_to = [], [], [] 79 data[key] = C.bool(data[key])
80 for contact, attrs, groups in contacts: 80 data['groups'] = list(groups)
81 from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) 81 contacts_dict[jid.JID(contact_jid_s)] = data
82 if not from_: 82
83 if not to: 83 await self.output(contacts_dict)
84 no_sub.append(contact)
85 elif self.args.no_from:
86 no_from.append(contact)
87 elif not to and self.args.no_to:
88 no_to.append(contact)
89 if not no_sub and not no_from and not no_to:
90 print("Nothing to do - there's a from and/or to subscription(s) between profile [%s] and each of its contacts" % self.host.profile)
91 elif self.ask_confirmation(no_sub, no_from, no_to):
92 for contact in no_sub + no_from + no_to:
93 self.host.bridge.delContact(contact, profile_key=self.host.profile, callback=lambda __: None, errback=lambda failure: None)
94 self.host.quit() 84 self.host.quit()
95 85
96 86
97 class Stats(base.CommandBase): 87 class Stats(base.CommandBase):
98 88
99 def __init__(self, host): 89 def __init__(self, host):
100 super(Stats, self).__init__(host, 'stats', help=_('Show statistics about a roster')) 90 super(Stats, self).__init__(host, 'stats', help=_('Show statistics about a roster'))
101 self.need_loop = True
102 91
103 def add_parser_options(self): 92 def add_parser_options(self):
104 pass 93 pass
105 94
106 def start(self): 95 async def start(self):
107 self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) 96 try:
108 97 contacts = await self.host.bridge.getContacts(profile_key=self.host.profile)
109 def error(self, failure): 98 except Exception as e:
110 print((_("Error while retrieving the contacts [%s]") % failure)) 99 self.disp(f"error while retrieving the contacts: {e}", error=True)
111 self.host.quit(1) 100 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
112 101
113 def gotContacts(self, contacts):
114 """Process the list of contacts.
115
116 @param contacts(list[tuple]): list of contacts with their attributes and groups
117 """
118 hosts = {} 102 hosts = {}
119 unique_groups = set() 103 unique_groups = set()
120 no_sub, no_from, no_to, no_group, total_group_subscription = 0, 0, 0, 0, 0 104 no_sub, no_from, no_to, no_group, total_group_subscription = 0, 0, 0, 0, 0
121 for contact, attrs, groups in contacts: 105 for contact, attrs, groups in contacts:
122 from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) 106 from_, to = C.bool(attrs["from"]), C.bool(attrs["to"])
125 no_sub += 1 109 no_sub += 1
126 else: 110 else:
127 no_from += 1 111 no_from += 1
128 elif not to: 112 elif not to:
129 no_to += 1 113 no_to += 1
130 host = jid.JID(contact).host 114
115 host = jid.JID(contact).domain
116
131 hosts.setdefault(host, 0) 117 hosts.setdefault(host, 0)
132 hosts[host] += 1 118 hosts[host] += 1
133 if groups: 119 if groups:
134 unique_groups.update(groups) 120 unique_groups.update(groups)
135 total_group_subscription += len(groups) 121 total_group_subscription += len(groups)
140 print() 126 print()
141 print("Total number of contacts: %d" % len(contacts)) 127 print("Total number of contacts: %d" % len(contacts))
142 print("Number of different hosts: %d" % len(hosts)) 128 print("Number of different hosts: %d" % len(hosts))
143 print() 129 print()
144 for host, count in hosts.items(): 130 for host, count in hosts.items():
145 print("Contacts on {host}: {count} ({rate:.1f}%)".format(host=host, count=count, rate=100 * float(count) / len(contacts))) 131 print("Contacts on {host}: {count} ({rate:.1f}%)".format(
132 host=host, count=count, rate=100 * float(count) / len(contacts)))
146 print() 133 print()
147 print("Contacts with no 'from' subscription: %d" % no_from) 134 print("Contacts with no 'from' subscription: %d" % no_from)
148 print("Contacts with no 'to' subscription: %d" % no_to) 135 print("Contacts with no 'to' subscription: %d" % no_to)
149 print("Contacts with no subscription at all: %d" % no_sub) 136 print("Contacts with no subscription at all: %d" % no_sub)
150 print() 137 print()
156 print("Average contacts per group: {:.1f}".format(contacts_per_group)) 143 print("Average contacts per group: {:.1f}".format(contacts_per_group))
157 try: 144 try:
158 groups_per_contact = float(total_group_subscription) / len(contacts) 145 groups_per_contact = float(total_group_subscription) / len(contacts)
159 except ZeroDivisionError: 146 except ZeroDivisionError:
160 groups_per_contact = 0 147 groups_per_contact = 0
161 print("Average groups' subscriptions per contact: {:.1f}".format(groups_per_contact)) 148 print(f"Average groups' subscriptions per contact: {groups_per_contact:.1f}")
162 print("Contacts not assigned to any group: %d" % no_group) 149 print("Contacts not assigned to any group: %d" % no_group)
163 self.host.quit() 150 self.host.quit()
164 151
165 152
166 class Get(base.CommandBase): 153 class Purge(base.CommandBase):
167 154
168 def __init__(self, host): 155 def __init__(self, host):
169 super(Get, self).__init__(host, 'get', help=_('Retrieve the roster contacts')) 156 super(Purge, self).__init__(
170 self.need_loop = True 157 host, 'purge',
171 158 help=_('purge the roster from its contacts with no subscription'))
172 def add_parser_options(self): 159
173 self.parser.add_argument("--subscriptions", action="store_true", help=_("Show the contacts' subscriptions")) 160 def add_parser_options(self):
174 self.parser.add_argument("--groups", action="store_true", help=_("Show the contacts' groups")) 161 self.parser.add_argument(
175 self.parser.add_argument("--name", action="store_true", help=_("Show the contacts' names")) 162 "--no_from", action="store_true",
176 163 help=_("also purge contacts with no 'from' subscription"))
177 def start(self): 164 self.parser.add_argument(
178 self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) 165 "--no_to", action="store_true",
179 166 help=_("also purge contacts with no 'to' subscription"))
180 def error(self, failure): 167
181 print((_("Error while retrieving the contacts [%s]") % failure)) 168 async def start(self):
182 self.host.quit(1) 169 try:
183 170 contacts = await self.host.bridge.getContacts(self.host.profile)
184 def gotContacts(self, contacts): 171 except Exception as e:
185 """Process the list of contacts. 172 self.disp(f"error while retrieving the contacts: {e}", error=True)
186 173 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
187 @param contacts(list[tuple]): list of contacts with their attributes and groups 174
175 no_sub, no_from, no_to = [], [], []
176 for contact, attrs, groups in contacts:
177 from_, to = C.bool(attrs["from"]), C.bool(attrs["to"])
178 if not from_:
179 if not to:
180 no_sub.append(contact)
181 elif self.args.no_from:
182 no_from.append(contact)
183 elif not to and self.args.no_to:
184 no_to.append(contact)
185 if not no_sub and not no_from and not no_to:
186 self.disp(
187 f"Nothing to do - there's a from and/or to subscription(s) between "
188 f"profile {self.host.profile!r} and each of its contacts"
189 )
190 elif await self.ask_confirmation(no_sub, no_from, no_to):
191 for contact in no_sub + no_from + no_to:
192 try:
193 await self.host.bridge.delContact(
194 contact, profile_key=self.host.profile)
195 except Exception as e:
196 self.disp(f"can't delete contact {contact!r}: {e}", error=True)
197 else:
198 self.disp(f"contact {contact!r} has been removed")
199
200 self.host.quit()
201
202 async def ask_confirmation(self, no_sub, no_from, no_to):
203 """Ask the confirmation before removing contacts.
204
205 @param no_sub (list[unicode]): list of contacts with no subscription
206 @param no_from (list[unicode]): list of contacts with no 'from' subscription
207 @param no_to (list[unicode]): list of contacts with no 'to' subscription
208 @return bool
188 """ 209 """
189 field_count = 1 # only display the contact by default 210 if no_sub:
190 if self.args.subscriptions: 211 self.disp(
191 field_count += 3 # ask, from, to 212 f"There's no subscription between profile {self.host.profile!r} and the "
192 if self.args.name: 213 f"following contacts:")
193 field_count += 1 214 self.disp(" " + "\n ".join(no_sub))
194 if self.args.groups: 215 if no_from:
195 field_count += 1 216 self.disp(
196 for contact, attrs, groups in contacts: 217 f"There's no 'from' subscription between profile {self.host.profile!r} "
197 args = [contact] 218 f"and the following contacts:")
198 if self.args.subscriptions: 219 self.disp(" " + "\n ".join(no_from))
199 args.append("ask" if C.bool(attrs["ask"]) else "") 220 if no_to:
200 args.append("from" if C.bool(attrs["from"]) else "") 221 self.disp(
201 args.append("to" if C.bool(attrs["to"]) else "") 222 f"There's no 'to' subscription between profile {self.host.profile!r} and "
202 if self.args.name: 223 f"the following contacts:")
203 args.append(str(attrs.get("name", ""))) 224 self.disp(" " + "\n ".join(no_to))
204 if self.args.groups: 225 message = f"REMOVE them from profile {self.host.profile}'s roster"
205 args.append("\t".join(groups) if groups else "") 226 while True:
206 print(";".join(["{}"] * field_count).format(*args).encode("utf-8")) 227 res = await self.host.ainput(f"{message} (y/N)? ")
207 self.host.quit() 228 if not res or res.lower() == 'n':
229 return False
230 if res.lower() == 'y':
231 return True
208 232
209 233
210 class Resync(base.CommandBase): 234 class Resync(base.CommandBase):
211 235
212 def __init__(self, host): 236 def __init__(self, host):
213 super(Resync, self).__init__( 237 super(Resync, self).__init__(
214 host, 'resync', help=_('do a full resynchronisation of roster with server')) 238 host, 'resync', help=_('do a full resynchronisation of roster with server'))
215 self.need_loop = True
216 239
217 def add_parser_options(self): 240 def add_parser_options(self):
218 pass 241 pass
219 242
220 def rosterResyncCb(self): 243 async def start(self):
221 self.disp(_("Roster resynchronized")) 244 try:
222 self.host.quit(C.EXIT_OK) 245 await self.host.bridge.rosterResync(profile_key=self.host.profile)
223 246 except Exception as e:
224 def start(self): 247 self.disp(f"can't resynchronise roster: {e}", error=True)
225 self.host.bridge.rosterResync(profile_key=self.host.profile, 248 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
226 callback=self.rosterResyncCb, 249 else:
227 errback=partial( 250 self.disp(_("Roster resynchronized"))
228 self.errback, 251 self.host.quit(C.EXIT_OK)
229 msg=_("can't resynchronise roster: {}"),
230 exit_code=C.EXIT_BRIDGE_ERRBACK,
231 ))
232 252
233 253
234 class Roster(base.CommandBase): 254 class Roster(base.CommandBase):
235 subcommands = (Get, Stats, Purge, Resync) 255 subcommands = (Get, Stats, Purge, Resync)
236 256
237 def __init__(self, host): 257 def __init__(self, host):
238 super(Roster, self).__init__(host, 'roster', use_profile=True, help=_("Manage an entity's roster")) 258 super(Roster, self).__init__(
259 host, 'roster', use_profile=True, help=_("Manage an entity's roster"))