Mercurial > libervia-backend
comparison frontends/src/quick_frontend/quick_contact_list.py @ 1367:f71a0fc26886
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 10:52:28 +0100 |
parents | ec9e58357a07 |
children | 017270e6eea4 |
comparison
equal
deleted
inserted
replaced
1295:1e3b1f9ad6e2 | 1367:f71a0fc26886 |
---|---|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 from sat.core.log import getLogger | 21 from sat.core.log import getLogger |
22 log = getLogger(__name__) | 22 log = getLogger(__name__) |
23 | 23 from sat_frontends.quick_frontend.quick_widgets import QuickWidget |
24 | 24 from sat_frontends.quick_frontend.constants import Const as C |
25 class QuickContactList(object): | 25 from sat_frontends.tools import jid |
26 | |
27 | |
28 try: | |
29 # FIXME: to be removed when an acceptable solution is here | |
30 unicode('') # XXX: unicode doesn't exist in pyjamas | |
31 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
32 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself | |
33 pyjamas_max = max | |
34 def max(iterable, key): | |
35 iter_cpy = list(iterable) | |
36 iter_cpy.sort(key=key) | |
37 return pyjamas_max(iter_cpy) | |
38 | |
39 | |
40 class QuickContactList(QuickWidget): | |
26 """This class manage the visual representation of contacts""" | 41 """This class manage the visual representation of contacts""" |
27 | 42 |
28 def __init__(self): | 43 def __init__(self, host, profile): |
29 log.debug(_("Contact List init")) | 44 log.debug(_("Contact List init")) |
45 super(QuickContactList, self).__init__(host, profile, profile) | |
46 # bare jids as keys, resources are used in data | |
30 self._cache = {} | 47 self._cache = {} |
31 self.specials={} | 48 |
32 | 49 # special entities (groupchat, gateways, etc), bare jids |
33 def update_jid(self, jid): | 50 self._specials = set() |
34 """Update the jid in the list when something changed""" | 51 # extras are specials with full jids (e.g.: private MUC conversation) |
52 self._special_extras = set() | |
53 | |
54 # group data contain jids in groups and misc frontend data | |
55 self._groups = {} # groups to group data map | |
56 | |
57 # contacts in roster (bare jids) | |
58 self._roster = set() | |
59 | |
60 # entities with an alert (usually a waiting message), full jid | |
61 self._alerts = set() | |
62 | |
63 # selected entities, full jid | |
64 self._selected = set() | |
65 | |
66 # we keep our own jid | |
67 self.whoami = host.profiles[profile].whoami | |
68 | |
69 # options | |
70 self.show_disconnected = False | |
71 self.show_empty_groups = True | |
72 self.show_resources = False | |
73 self.show_status = False | |
74 # TODO: this may lead to two successive UI refresh and needs an optimization | |
75 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) | |
76 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) | |
77 | |
78 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) | |
79 self.presenceListener = self.onPresenceUpdate | |
80 self.host.addListener('presence', self.presenceListener, [profile]) | |
81 self.nickListener = self.onNickUpdate | |
82 self.host.addListener('nick', self.nickListener, [profile]) | |
83 | |
84 def __contains__(self, entity): | |
85 """Check if entity is in contact list | |
86 | |
87 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) | |
88 """ | |
89 if entity.resource: | |
90 try: | |
91 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) | |
92 except KeyError: | |
93 return False | |
94 return entity in self._cache | |
95 | |
96 @property | |
97 def roster_entities(self): | |
98 """Return all the bare JIDs of the roster entities. | |
99 | |
100 @return: set(jid.JID) | |
101 """ | |
102 return self._roster | |
103 | |
104 @property | |
105 def roster_entities_connected(self): | |
106 """Return all the bare JIDs of the roster entities that are connected. | |
107 | |
108 @return: set(jid.JID) | |
109 """ | |
110 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) | |
111 | |
112 @property | |
113 def roster_entities_by_group(self): | |
114 """Return a dictionary binding the roster groups to their entities bare | |
115 JIDs. This also includes the empty group (None key). | |
116 | |
117 @return: dict{unicode: set(jid.JID)} | |
118 """ | |
119 return {group: self._groups[group]['jids'] for group in self._groups} | |
120 | |
121 @property | |
122 def roster_groups_by_entity(self): | |
123 """Return a dictionary binding the entities bare JIDs to their roster | |
124 groups. The empty group is filtered out. | |
125 | |
126 @return: dict{jid.JID: set(unicode)} | |
127 """ | |
128 result = {} | |
129 for group, data in self._groups.iteritems(): | |
130 if group is None: | |
131 continue | |
132 for entity in data['jids']: | |
133 result.setdefault(entity, set()).add(group) | |
134 return result | |
135 | |
136 def fill(self): | |
137 """Get all contacts from backend, and fill the widget""" | |
138 def gotContacts(contacts): | |
139 for contact in contacts: | |
140 self.host.newContactHandler(*contact, profile=self.profile) | |
141 | |
142 self.host.bridge.getContacts(self.profile, callback=gotContacts) | |
143 | |
144 def update(self): | |
145 """Update the display when something changed""" | |
35 raise NotImplementedError | 146 raise NotImplementedError |
36 | 147 |
37 def getCache(self, jid, name): | 148 def getCache(self, entity, name=None): |
149 """Return a cache value for a contact | |
150 | |
151 @param entity(entity.entity): entity of the contact from who we want data (resource is used if given) | |
152 if a resource specific information is requested: | |
153 - if no resource is given (bare jid), the main resource is used, according to priority | |
154 - if resource is given, it is used | |
155 @param name(unicode): name the data to get, or None to get everything | |
156 @return: full cache if no name is given, or value of "name", or None | |
157 """ | |
38 try: | 158 try: |
39 jid_cache = self._cache[jid.bare] | 159 cache = self._cache[entity.bare] |
40 if name == 'status': #XXX: we get the first status for 'status' key | 160 except KeyError: |
41 return jid_cache['statuses'].get('default','') | 161 self.setContact(entity) |
42 return jid_cache[name] | 162 cache = self._cache[entity.bare] |
43 except (KeyError, IndexError): | 163 |
164 if name is None: | |
165 return cache | |
166 try: | |
167 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): | |
168 # these data are related to the resource | |
169 if not entity.resource: | |
170 main_resource = cache[C.CONTACT_MAIN_RESOURCE] | |
171 cache = cache[C.CONTACT_RESOURCES][main_resource] | |
172 else: | |
173 cache = cache[C.CONTACT_RESOURCES][entity.resource] | |
174 | |
175 if name == 'status': #XXX: we get the first status for 'status' key | |
176 # TODO: manage main language for statuses | |
177 return cache[C.PRESENCE_STATUSES].get('default','') | |
178 | |
179 return cache[name] | |
180 except KeyError: | |
44 return None | 181 return None |
45 | 182 |
46 def setCache(self, jid, name, value): | 183 def setCache(self, entity, name, value): |
47 jid_cache = self._cache.setdefault(jid.bare, {}) | 184 """Set or update value for one data in cache |
48 jid_cache[name] = value | 185 |
49 | 186 @param entity(JID): entity to update |
50 def __contains__(self, jid): | 187 @param name(unicode): value to set or update |
51 raise NotImplementedError | 188 """ |
189 self.setContact(entity, None, {name: value}) | |
190 | |
191 def getFullJid(self, entity): | |
192 """Get full jid from a bare jid | |
193 | |
194 @param entity(jid.JID): must be a bare jid | |
195 @return (jid.JID): bare jid + main resource | |
196 @raise ValueError: the entity is not bare | |
197 """ | |
198 if entity.resource: | |
199 raise ValueError("getFullJid must be used with a bare jid") | |
200 main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE) | |
201 return jid.JID(u"{}/{}".format(entity, main_resource)) | |
202 | |
203 | |
204 def setGroupData(self, group, name, value): | |
205 """Register a data for a group | |
206 | |
207 @param group: a valid (existing) group name | |
208 @param name: name of the data (can't be "jids") | |
209 @param value: value to set | |
210 """ | |
211 assert name is not 'jids' | |
212 self._groups[group][name] = value | |
213 | |
214 def getGroupData(self, group, name=None): | |
215 """Return value associated to group data | |
216 | |
217 @param group: a valid (existing) group name | |
218 @param name: name of the data or None to get the whole dict | |
219 @return: registered value | |
220 """ | |
221 if name is None: | |
222 return self._groups[group] | |
223 return self._groups[group][name] | |
224 | |
225 def setSpecial(self, entity, special_type): | |
226 """Set special flag on an entity | |
227 | |
228 @param entity(jid.JID): jid of the special entity | |
229 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag | |
230 """ | |
231 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) | |
232 self.setCache(entity, C.CONTACT_SPECIAL, special_type) | |
233 | |
234 def getSpecials(self, special_type=None): | |
235 """Return all the bare JIDs of the special roster entities of the type | |
236 specified by special_type. If special_type is None, return all specials. | |
237 | |
238 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials. | |
239 @return: set(jid.JID) | |
240 """ | |
241 if special_type is None: | |
242 return self._specials | |
243 return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type]) | |
52 | 244 |
53 def clearContacts(self): | 245 def clearContacts(self): |
54 """Clear all the contact list""" | 246 """Clear all the contact list""" |
55 self.specials.clear() | 247 self.unselectAll() |
56 | 248 self._cache.clear() |
57 def replace(self, jid, groups=None, attributes=None): | 249 self._groups.clear() |
250 self._specials.clear() | |
251 self.update() | |
252 | |
253 def setContact(self, entity, groups=None, attributes=None, in_roster=False): | |
58 """Add a contact to the list if doesn't exist, else update it. | 254 """Add a contact to the list if doesn't exist, else update it. |
59 | 255 |
60 This method can be called with groups=None for the purpose of updating | 256 This method can be called with groups=None for the purpose of updating |
61 the contact's attributes (e.g. nickname). In that case, the groups | 257 the contact's attributes (e.g. nickname). In that case, the groups |
62 attribute must not be set to the default group but ignored. If not, | 258 attribute must not be set to the default group but ignored. If not, |
63 you may move your contact from its actual group(s) to the default one. | 259 you may move your contact from its actual group(s) to the default one. |
64 | 260 |
65 None value for 'groups' has a different meaning than [None] which is for the default group. | 261 None value for 'groups' has a different meaning than [None] which is for the default group. |
66 | 262 |
67 @param jid (JID) | 263 @param entity (jid.JID): entity to add or replace |
68 @param groups (list): list of groups or None to ignore the groups membership. | 264 @param groups (list): list of groups or None to ignore the groups membership. |
69 @param attributes (dict) | 265 @param attributes (dict): attibutes of the added jid or to update |
70 """ | 266 @param in_roster (bool): True if contact is from roster |
71 if attributes and 'name' in attributes: | 267 """ |
72 self.setCache(jid, 'name', attributes['name']) | 268 if attributes is None: |
73 | 269 attributes = {} |
74 def remove(self, jid): | 270 |
75 """remove a contact from the list""" | 271 entity_bare = entity.bare |
272 | |
273 if in_roster: | |
274 self._roster.add(entity_bare) | |
275 | |
276 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}}) | |
277 | |
278 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes | |
279 | |
280 # we set groups and fill self._groups accordingly | |
281 if groups is not None: | |
282 if not groups: | |
283 groups = [None] # [None] is the default group | |
284 if C.CONTACT_GROUPS in cache: | |
285 # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS] | |
286 for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]: | |
287 self._groups[group]['jids'].remove(entity_bare) | |
288 cache[C.CONTACT_GROUPS] = groups | |
289 for group in groups: | |
290 self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare) | |
291 | |
292 # special entities management | |
293 if C.CONTACT_SPECIAL in attributes: | |
294 if attributes[C.CONTACT_SPECIAL] is None: | |
295 del attributes[C.CONTACT_SPECIAL] | |
296 self._specials.remove(entity_bare) | |
297 else: | |
298 self._specials.add(entity_bare) | |
299 | |
300 # now the attribute we keep in cache | |
301 for attribute, value in attributes.iteritems(): | |
302 cache[attribute] = value | |
303 | |
304 # we can update the display | |
305 self.update() | |
306 | |
307 def getContacts(self): | |
308 """Return contacts currently selected | |
309 | |
310 @return (set): set of selected entities""" | |
311 return self._selected | |
312 | |
313 def entityToShow(self, entity, check_resource=False): | |
314 """Tell if the contact should be showed or hidden. | |
315 | |
316 @param entity (jid.JID): jid of the contact | |
317 @param check_resource (bool): True if resource must be significant | |
318 @return (bool): True if that contact should be showed in the list | |
319 """ | |
320 show = self.getCache(entity, C.PRESENCE_SHOW) | |
321 | |
322 if check_resource: | |
323 alerts = self._alerts | |
324 selected = self._selected | |
325 else: | |
326 alerts = {alert.bare for alert in self._alerts} | |
327 selected = {selected.bare for selected in self._selected} | |
328 return ((show is not None and show != "unavailable") | |
329 or self.show_disconnected | |
330 or entity in alerts | |
331 or entity in selected) | |
332 | |
333 def anyEntityToShow(self, entities, check_resources=False): | |
334 """Tell if in a list of entities, at least one should be shown | |
335 | |
336 @param entities (list[jid.JID]): list of jids | |
337 @param check_resources (bool): True if resources must be significant | |
338 @return: bool | |
339 """ | |
340 for entity in entities: | |
341 if self.entityToShow(entity, check_resources): | |
342 return True | |
343 return False | |
344 | |
345 def isEntityInGroup(self, entity, group): | |
346 """Tell if an entity is in a roster group | |
347 | |
348 @param entity(jid.JID): jid of the entity | |
349 @param group(unicode): group to check | |
350 @return (bool): True if the entity is in the group | |
351 """ | |
352 return entity in self.getGroupData(group, "jids") | |
353 | |
354 def remove(self, entity): | |
355 """remove a contact from the list | |
356 | |
357 @param entity(jid.JID): jid of the entity to remove (bare jid is used) | |
358 """ | |
359 entity_bare = entity.bare | |
76 try: | 360 try: |
77 del self.specials[jid.bare] | 361 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) |
78 except KeyError: | 362 except KeyError: |
79 pass | 363 log.warning(_("Trying to delete an unknow entity [{}]").format(entity)) |
80 | 364 del self._cache[entity_bare] |
81 def add(self, jid, param_groups=None): | 365 for group in groups: |
82 """add a contact to the list""" | 366 self._groups[group]['jids'].remove(entity_bare) |
83 raise NotImplementedError | 367 for set_ in (self._selected, self._alerts, self._specials, self._special_extras): |
84 | 368 to_remove = set() |
85 def getSpecial(self, jid): | 369 for set_entity in set_: |
86 """Return special type of jid, or None if it's not special""" | 370 if set_entity.bare == entity.bare: |
87 return self.specials.get(jid.bare) | 371 to_remove.add(set_entity) |
88 | 372 set_.difference_update(to_remove) |
89 def setSpecial(self, jid, _type, show=False): | 373 self.update() |
90 """Set entity as a special | 374 |
91 @param jid: jid of the entity | 375 def onPresenceUpdate(self, entity, show, priority, statuses, profile): |
92 @param _type: special type (e.g.: "MUC") | |
93 @param show: True to display the dialog to chat with this entity | |
94 """ | |
95 self.specials[jid.bare] = _type | |
96 | |
97 def updatePresence(self, jid, show, priority, statuses): | |
98 """Update entity's presence status | 376 """Update entity's presence status |
99 @param jid: entity to update's jid | 377 |
378 @param entity(jid.JID): entity updated | |
100 @param show: availability | 379 @param show: availability |
101 @parap priority: resource's priority | 380 @parap priority: resource's priority |
102 @param statuses: dict of statuses""" | 381 @param statuses: dict of statuses |
103 self.setCache(jid, 'show', show) | 382 @param profile: %(doc_profile)s |
104 self.setCache(jid, 'prority', priority) | 383 """ |
105 self.setCache(jid, 'statuses', statuses) | 384 cache = self.getCache(entity) |
106 self.update_jid(jid) | 385 if show == C.PRESENCE_UNAVAILABLE: |
386 if not entity.resource: | |
387 cache[C.CONTACT_RESOURCES].clear() | |
388 cache[C.CONTACT_MAIN_RESOURCE]= None | |
389 else: | |
390 try: | |
391 del cache[C.CONTACT_RESOURCES][entity.resource] | |
392 except KeyError: | |
393 log.error("Presence unavailable received for an unknown resource [{}]".format(entity)) | |
394 if not cache[C.CONTACT_RESOURCES]: | |
395 cache[C.CONTACT_MAIN_RESOURCE] = None | |
396 else: | |
397 assert entity.resource | |
398 resources_data = cache[C.CONTACT_RESOURCES] | |
399 resource_data = resources_data.setdefault(entity.resource, {}) | |
400 resource_data[C.PRESENCE_SHOW] = show | |
401 resource_data[C.PRESENCE_PRIORITY] = int(priority) | |
402 resource_data[C.PRESENCE_STATUSES] = statuses | |
403 | |
404 priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY]) | |
405 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource | |
406 | |
407 def onNickUpdate(self, entity, new_nick, profile): | |
408 """Update entity's nick | |
409 | |
410 @param entity(jid.JID): entity updated | |
411 @param new_nick(unicode): new nick of the entity | |
412 @param profile: %(doc_profile)s | |
413 """ | |
414 raise NotImplementedError # Must be implemented by frontends | |
415 | |
416 def unselectAll(self): | |
417 """Unselect all contacts""" | |
418 self._selected.clear() | |
419 self.update() | |
420 | |
421 def select(self, entity): | |
422 """Select an entity | |
423 | |
424 @param entity(jid.JID): entity to select (resource is significant) | |
425 """ | |
426 log.debug("select %s" % entity) | |
427 self._selected.add(entity) | |
428 self.update() | |
429 | |
430 def setAlert(self, entity): | |
431 """Set an alert on the entity (usually for a waiting message) | |
432 | |
433 @param entity(jid.JID): entity which must displayed in alert mode (resource is significant) | |
434 """ | |
435 self._alerts.add(entity) | |
436 self.update() | |
437 | |
438 def _showOfflineContacts(self, show_str): | |
439 self.showOfflineContacts(C.bool(show_str)) | |
107 | 440 |
108 def showOfflineContacts(self, show): | 441 def showOfflineContacts(self, show): |
109 pass | 442 """Tell if offline contacts should shown |
443 | |
444 @param show(bool): True if offline contacts should be shown | |
445 """ | |
446 assert isinstance(show, bool) | |
447 if self.show_disconnected == show: | |
448 return | |
449 self.show_disconnected = show | |
450 self.update() | |
451 | |
452 def _showEmptyGroups(self, show_str): | |
453 self.showEmptyGroups(C.bool(show_str)) | |
110 | 454 |
111 def showEmptyGroups(self, show): | 455 def showEmptyGroups(self, show): |
112 pass | 456 assert isinstance(show, bool) |
457 if self.show_empty_groups == show: | |
458 return | |
459 self.show_empty_groups = show | |
460 self.update() | |
461 | |
462 def showResources(self, show): | |
463 show = C.bool(show) | |
464 if self.show_resources == show: | |
465 return | |
466 self.show_resources = show | |
467 self.update() | |
468 | |
469 def onDelete(self): | |
470 QuickWidget.onDelete(self) | |
471 self.host.removeListener('presence', self.presenceListener) |