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)