Mercurial > libervia-backend
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")) |