Mercurial > libervia-backend
view libervia/frontends/quick_frontend/quick_contact_list.py @ 4240:79c8a70e1813
backend, frontend: prepare remote control:
This is a series of changes necessary to prepare the implementation of remote control
feature:
- XEP-0166: add a `priority` attribute to `ApplicationData`: this is needed when several
applications are working in a same session, to know which one must be handled first.
Will be used to make Remote Control have precedence over Call content.
- XEP-0166: `_call_plugins` is now async and is not used with `DeferredList` anymore: the
benefit to have methods called in parallels is very low, and it cause a lot of trouble
as we can't predict order. Methods are now called sequentially so workflow can be
predicted.
- XEP-0167: fix `senders` XMPP attribute <=> SDP mapping
- XEP-0234: preflight acceptance key is now `pre-accepted` instead of `file-accepted`, so
the same key can be used with other jingle applications.
- XEP-0167, XEP-0343: move some method to XEP-0167
- XEP-0353: use new `priority` feature to call preflight methods of applications according
to it.
- frontend (webrtc): refactor the sources/sink handling with a more flexible mechanism
based on Pydantic models. It is now possible to have has many Data Channel as necessary,
to have them in addition to A/V streams, to specify manually GStreamer sources and
sinks, etc.
- frontend (webrtc): rework of the pipeline to reduce latency.
- frontend: new `portal_desktop` method. Screenshare portal handling has been moved there,
and RemoteDesktop portal has been added.
- frontend (webrtc): fix `extract_ufrag_pwd` method.
rel 436
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 11 May 2024 13:52:41 +0200 |
parents | b47f21f2b8fa |
children | 0d7bb4df2343 |
line wrap: on
line source
#!/usr/bin/env python3 # helper class for making a SàT frontend contact lists # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """Contact List handling multi profiles at once, should replace quick_contact_list module in the future""" from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger from libervia.backend.core import exceptions from libervia.frontends.quick_frontend.quick_widgets import QuickWidget from libervia.frontends.quick_frontend.constants import Const as C from libervia.frontends.tools import jid from collections import OrderedDict log = getLogger(__name__) try: # FIXME: to be removed when an acceptable solution is here str("") # XXX: unicode doesn't exist in pyjamas except (TypeError, AttributeError): # Error raised is not the same depending on # pyjsbuild options # XXX: pyjamas' max doesn't support key argument, so we implement it ourself pyjamas_max = max def max(iterable, key): iter_cpy = list(iterable) iter_cpy.sort(key=key) return pyjamas_max(iter_cpy) # next doesn't exist in pyjamas def next(iterable, *args): try: return iterable.__next__() except StopIteration as e: if args: return args[0] raise e handler = None class ProfileContactList(object): """Contact list data for a single profile""" def __init__(self, profile): self.host = handler.host self.profile = profile # contain all jids in roster or not, # bare jids as keys, resources are used in data # XXX: we don't mutualise cache, as values may differ # for different profiles (e.g. directed presence) self._cache = {} # special entities (groupchat, gateways, etc) # may be bare or full jid self._specials = set() # group data contain jids in groups and misc frontend data # None key is used for jids with no group self._groups = {} # groups to group data map # contacts in roster (bare jids) self._roster = set() # selected entities, full jid self._selected = set() # options self.show_disconnected = False self._show_empty_groups = True self._show_resources = False self.show_status = False # do we show entities with notifications? # if True, entities will be show even if they normally would not # (e.g. not in contact list) if they have notifications attached self.show_entities_with_notifs = True self.host.bridge.param_get_a_async( C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._show_empty_groups_cb, ) self.host.bridge.param_get_a_async( C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._show_offline_contacts, ) self.host.addListener("presence", self.on_presence_update, [self.profile]) self.host.addListener("nicknames", self.on_nicknames_update, [self.profile]) self.host.addListener("notification", self.on_notification, [self.profile]) # on_notification only updates the entity, so we can re-use it self.host.addListener("notificationsClear", self.on_notification, [self.profile]) @property def whoami(self): return self.host.profiles[self.profile].whoami def _show_empty_groups_cb(self, show_str): # Called only by __init__ # self.update is not wanted here, as it is done by # handler when all profiles are ready self.show_empty_groups(C.bool(show_str)) def _show_offline_contacts(self, show_str): # same comments as for _show_empty_groups self.show_offline_contacts(C.bool(show_str)) def __contains__(self, entity): """Check if entity is in contact list An entity can be in contact list even if not in roster use is_in_roster to check if entity is in roster. @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) """ if entity.resource: try: return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) except exceptions.NotFound: return False return entity in self._cache @property def roster(self): """Return all the bare JIDs of the roster entities. @return (set[jid.JID]) """ return self._roster @property def roster_connected(self): """Return all the bare JIDs of the roster entities that are connected. @return (set[jid.JID]) """ return set( [ entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW, default=None) is not None ] ) @property def roster_entities_by_group(self): """Return a dictionary binding the roster groups to their entities bare JIDs. This also includes the empty group (None key). @return (dict[unicode,set(jid.JID)]) """ return {group: self._groups[group]["jids"] for group in self._groups} @property def roster_groups_by_entities(self): """Return a dictionary binding the entities bare JIDs to their roster groups @return (dict[jid.JID, set(unicode)]) """ result = {} for group, data in self._groups.items(): for entity in data["jids"]: result.setdefault(entity, set()).add(group) return result @property def selected(self): """Return contacts currently selected @return (set): set of selected entities """ return self._selected @property def all_iter(self): """return all know entities in cache as an iterator of tuples entities are not sorted """ return iter(self._cache.items()) @property def items(self): """Return item representation for all visible entities in cache entities are not sorted key: bare jid, value: data """ return { jid_: cache for jid_, cache in self._cache.items() if self.entity_visible(jid_) } def get_item(self, entity): """Return item representation of requested entity @param entity(jid.JID): bare jid of entity @raise (KeyError): entity is unknown """ return self._cache[entity] def _got_contacts(self, contacts): """Add contacts and notice parent that contacts are filled Called during initial contact list filling @param contacts(tuple): all contacts """ for contact in contacts: entity = jid.JID(contact[0]) if entity.resource: # we use entity's bare jid to cache data, so a resource here # will cause troubles log.warning( "Roster entities with resources are not managed, ignoring {entity}" .format(entity=entity)) continue self.host.contact_new_handler(*contact, profile=self.profile) handler._contacts_filled(self.profile) def _fill(self): """Get all contacts from backend Contacts will be cleared before refilling them """ self.clear_contacts(keep_cache=True) self.host.bridge.contacts_get(self.profile, callback=self._got_contacts) def fill(self): handler.fill(self.profile) def getCache( self, entity, name=None, bare_default=True, create_if_not_found=False, default=Exception): """Return a cache value for a contact @param entity(jid.JID): entity of the contact from who we want data (resource is used if given) if a resource specific information is requested: - if no resource is given (bare jid), the main resource is used, according to priority - if resource is given, it is used @param name(unicode): name the data to get, or None to get everything @param bare_default(bool, None): if True and entity is a full jid, the value of bare jid will be returned if not value is found for the requested resource. If False, None is returned if no value is found for the requested resource. If None, bare_default will be set to False if entity is in a room, True else @param create_if_not_found(bool): if True, create contact if it's not found in cache @param default(object): value to return when name is not found in cache if Exception is used, a KeyError will be returned otherwise, the given value will be used @return: full cache if no name is given, or value of "name", or None @raise NotFound: entity not found in cache @raise KeyError: name not found in cache """ # FIXME: resource handling need to be reworked # FIXME: bare_default work for requesting full jid to get bare jid, # but not the other way # e.g.: if we have set an avatar for user@server.tld/resource # and we request user@server.tld # we won't get the avatar set in the resource try: cache = self._cache[entity.bare] except KeyError: if create_if_not_found: self.set_contact(entity) cache = self._cache[entity.bare] else: raise exceptions.NotFound if name is None: if default is not Exception: raise exceptions.InternalError( "default value can only Exception when name is not specified" ) # full cache is requested return cache if name in ("status", C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): # these data are related to the resource if not entity.resource: main_resource = cache[C.CONTACT_MAIN_RESOURCE] if main_resource is None: # we ignore presence info if we don't have any resource in cache # FIXME: to be checked return cache = cache[C.CONTACT_RESOURCES].setdefault(main_resource, {}) else: cache = cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) if name == "status": # XXX: we get the first status for 'status' key # TODO: manage main language for statuses return cache[C.PRESENCE_STATUSES].get(C.PRESENCE_STATUSES_DEFAULT, "") elif entity.resource: try: return cache[C.CONTACT_RESOURCES][entity.resource][name] except KeyError as e: if bare_default is None: bare_default = not self.is_room(entity.bare) if not bare_default: if default is Exception: raise e else: return default try: return cache[name] except KeyError as e: if default is Exception: raise e else: return default def set_cache(self, entity, name, value): """Set or update value for one data in cache @param entity(JID): entity to update @param name(str): value to set or update """ self.set_contact(entity, attributes={name: value}) def get_full_jid(self, entity): """Get full jid from a bare jid @param entity(jid.JID): must be a bare jid @return (jid.JID): bare jid + main resource @raise ValueError: the entity is not bare """ if entity.resource: raise ValueError("get_full_jid must be used with a bare jid") main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE) return jid.JID("{}/{}".format(entity, main_resource)) def set_group_data(self, group, name, value): """Register a data for a group @param group: a valid (existing) group name @param name: name of the data (can't be "jids") @param value: value to set """ assert name != "jids" self._groups[group][name] = value def get_group_data(self, group, name=None): """Return value associated to group data @param group: a valid (existing) group name @param name: name of the data or None to get the whole dict @return: registered value """ if name is None: return self._groups[group] return self._groups[group][name] def is_in_roster(self, entity): """Tell if an entity is in roster @param entity(jid.JID): jid of the entity the bare jid will be used """ return entity.bare in self._roster def is_room(self, entity): """Helper method to know if entity is a MUC room @param entity(jid.JID): jid of the entity hint: use bare jid here, as room can't be full jid with MUC @return (bool): True if entity is a room """ assert entity.resource is None # FIXME: this may change when MIX will be handled return self.is_special(entity, C.CONTACT_SPECIAL_GROUP) def is_special(self, entity, special_type): """Tell if an entity is of a specialy _type @param entity(jid.JID): jid of the special entity if the jid is full, will be added to special extras @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) @return (bool): True if entity is from this special type """ return self.getCache(entity, C.CONTACT_SPECIAL, default=None) == special_type def set_special(self, entity, special_type): """Set special flag on an entity @param entity(jid.JID): jid of the special entity if the jid is full, will be added to special extras @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag """ assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) self.set_cache(entity, C.CONTACT_SPECIAL, special_type) def get_specials(self, special_type=None, bare=False): """Return all the bare JIDs of the special roster entities of with given type. @param special_type(unicode, None): if not None, filter by special type (e.g. C.CONTACT_SPECIAL_GROUP) @param bare(bool): return only bare jids if True @return (iter[jid.JID]): found special entities """ for entity in self._specials: if bare and entity.resource: continue if ( special_type is not None and self.getCache(entity, C.CONTACT_SPECIAL, default=None) != special_type ): continue yield entity def disconnect(self): # for now we just clear contacts on disconnect self.clear_contacts() def clear_contacts(self, keep_cache=False): """Clear all the contact list @param keep_cache: if True, don't reset the cache """ self.select(None) if not keep_cache: self._cache.clear() self._groups.clear() self._specials.clear() self._roster.clear() self.update() def set_contact(self, entity, groups=None, attributes=None, in_roster=False): """Add a contact to the list if it doesn't exist, else update it. This method can be called with groups=None for the purpose of updating the contact's attributes (e.g. nicknames). In that case, the groups attribute must not be set to the default group but ignored. If not, you may move your contact from its actual group(s) to the default one. None value for 'groups' has a different meaning than [None] which is for the default group. @param entity (jid.JID): entity to add or replace if entity is a full jid, attributes will be cached in for the full jid only @param groups (list): list of groups or None to ignore the groups membership. @param attributes (dict): attibutes of the added jid or to update @param in_roster (bool): True if contact is from roster """ if attributes is None: attributes = {} entity_bare = entity.bare # we check if the entity is visible before changing anything # this way we know if we need to do an UPDATE_ADD, UPDATE_MODIFY # or an UPDATE_DELETE was_visible = self.entity_visible(entity_bare) if in_roster: self._roster.add(entity_bare) cache = self._cache.setdefault( entity_bare, { C.CONTACT_RESOURCES: {}, C.CONTACT_MAIN_RESOURCE: None, C.CONTACT_SELECTED: set(), }, ) # we don't want forbidden data in attributes assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we set groups and fill self._groups accordingly if groups is not None: if not groups: groups = [None] # [None] is the default group if C.CONTACT_GROUPS in cache: # 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] for group in [ group for group in cache[C.CONTACT_GROUPS] if group not in groups ]: self._groups[group]["jids"].remove(entity_bare) cache[C.CONTACT_GROUPS] = groups for group in groups: self._groups.setdefault(group, {}).setdefault("jids", set()).add( entity_bare ) # special entities management if C.CONTACT_SPECIAL in attributes: if attributes[C.CONTACT_SPECIAL] is None: del attributes[C.CONTACT_SPECIAL] self._specials.remove(entity) else: self._specials.add(entity) cache[C.CONTACT_MAIN_RESOURCE] = None if 'nicknames' in cache: del cache['nicknames'] # now the attributes we keep in cache # XXX: if entity is a full jid, we store the value for the resource only cache_attr = ( cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) if entity.resource else cache ) for attribute, value in attributes.items(): if attribute == "nicknames" and self.is_special( entity, C.CONTACT_SPECIAL_GROUP ): # we don't want to keep nicknames for MUC rooms # FIXME: this is here as plugin XEP-0054 can link resource's nick # with bare jid which in the case of MUC # set the nick for the whole MUC # resulting in bad name displayed in some frontends # FIXME: with plugin XEP-0054 + plugin identity refactoring, this # may not be needed anymore… continue cache_attr[attribute] = value # we can update the display if needed if self.entity_visible(entity_bare): # if the contact was not visible, we need to add a widget # else we just update id update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD self.update([entity], update_type, self.profile) elif was_visible: # the entity was visible and is not anymore, we remove it self.update([entity], C.UPDATE_DELETE, self.profile) def entity_visible(self, entity, check_resource=False): """Tell if the contact should be showed or hidden. @param entity (jid.JID): jid of the contact @param check_resource (bool): True if resource must be significant @return (bool): True if that contact should be showed in the list """ try: show = self.getCache(entity, C.PRESENCE_SHOW) except (exceptions.NotFound, KeyError): return False if check_resource: selected = self._selected else: selected = {selected.bare for selected in self._selected} return ( (show is not None and show != C.PRESENCE_UNAVAILABLE) or self.show_disconnected or entity in selected or ( self.show_entities_with_notifs and next(self.host.get_notifs(entity.bare, profile=self.profile), None) ) or entity.resource is None and self.is_room(entity.bare) ) def any_entity_visible(self, entities, check_resources=False): """Tell if in a list of entities, at least one should be shown @param entities (list[jid.JID]): list of jids @param check_resources (bool): True if resources must be significant @return (bool): True if a least one entity need to be shown """ # FIXME: looks inefficient, really needed? for entity in entities: if self.entity_visible(entity, check_resources): return True return False def is_entity_in_group(self, entity, group): """Tell if an entity is in a roster group @param entity(jid.JID): jid of the entity @param group(unicode): group to check @return (bool): True if the entity is in the group """ return entity in self.get_group_data(group, "jids") def remove_contact(self, entity): """remove a contact from the list @param entity(jid.JID): jid of the entity to remove (bare jid is used) """ entity_bare = entity.bare was_visible = self.entity_visible(entity_bare) try: groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) except KeyError: log.error(_("Trying to delete an unknow entity [{}]").format(entity)) try: self._roster.remove(entity_bare) except KeyError: pass del self._cache[entity_bare] for group in groups: self._groups[group]["jids"].remove(entity_bare) if not self._groups[group]["jids"]: # FIXME: we use pop because of pyjamas: # http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en self._groups.pop(group) for iterable in (self._selected, self._specials): to_remove = set() for set_entity in iterable: if set_entity.bare == entity.bare: to_remove.add(set_entity) iterable.difference_update(to_remove) if was_visible: self.update([entity], C.UPDATE_DELETE, self.profile) def on_presence_update(self, entity, show, priority, statuses, profile): """Update entity's presence status @param entity(jid.JID): entity updated @param show: availability @parap priority: resource's priority @param statuses: dict of statuses @param profile: %(doc_profile)s """ # FIXME: cache modification should be done with set_contact # the resources/presence handling logic should be moved there was_visible = self.entity_visible(entity.bare) cache = self.getCache(entity, create_if_not_found=True) if show == C.PRESENCE_UNAVAILABLE: if not entity.resource: cache[C.CONTACT_RESOURCES].clear() cache[C.CONTACT_MAIN_RESOURCE] = None else: try: del cache[C.CONTACT_RESOURCES][entity.resource] except KeyError: log.error( "Presence unavailable received " "for an unknown resource [{}]".format(entity) ) if not cache[C.CONTACT_RESOURCES]: cache[C.CONTACT_MAIN_RESOURCE] = None else: if not entity.resource: log.warning( _( "received presence from entity " "without resource: {}".format(entity) ) ) resources_data = cache[C.CONTACT_RESOURCES] resource_data = resources_data.setdefault(entity.resource, {}) resource_data[C.PRESENCE_SHOW] = show resource_data[C.PRESENCE_PRIORITY] = int(priority) resource_data[C.PRESENCE_STATUSES] = statuses if entity.bare not in self._specials: # we may have resources with no priority # (when a cached value is added for a not connected resource) priority_resource = max( resources_data, key=lambda res: resources_data[res].get( C.PRESENCE_PRIORITY, -2 ** 32 ), ) cache[C.CONTACT_MAIN_RESOURCE] = priority_resource if self.entity_visible(entity.bare): update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD self.update([entity], update_type, self.profile) elif was_visible: self.update([entity], C.UPDATE_DELETE, self.profile) def on_nicknames_update(self, entity, nicknames, profile): """Update entity's nicknames @param entity(jid.JID): entity updated @param nicknames(list[unicode]): nicknames of the entity @param profile: %(doc_profile)s """ assert profile == self.profile self.set_cache(entity, "nicknames", nicknames) def on_notification(self, entity, notif, profile): """Update entity with notification @param entity(jid.JID): entity updated @param notif(dict): notification data @param profile: %(doc_profile)s """ assert profile == self.profile if entity is not None and self.entity_visible(entity): self.update([entity], C.UPDATE_MODIFY, profile) def unselect(self, entity): """Unselect an entity @param entity(jid.JID): entity to unselect """ try: cache = self._cache[entity.bare] except: log.error("Try to unselect an entity not in cache") else: try: cache[C.CONTACT_SELECTED].remove(entity.resource) except KeyError: log.error("Try to unselect a not selected entity") else: self._selected.remove(entity) self.update([entity], C.UPDATE_SELECTION) def select(self, entity): """Select an entity @param entity(jid.JID, None): entity to select (resource is significant) None to unselect all entities """ if entity is None: self._selected.clear() for cache in self._cache.values(): cache[C.CONTACT_SELECTED].clear() self.update(type_=C.UPDATE_SELECTION, profile=self.profile) else: log.debug("select %s" % entity) try: cache = self._cache[entity.bare] except: log.error("Try to select an entity not in cache") else: cache[C.CONTACT_SELECTED].add(entity.resource) self._selected.add(entity) self.update([entity], C.UPDATE_SELECTION, profile=self.profile) def show_offline_contacts(self, show): """Tell if offline contacts should be shown @param show(bool): True if offline contacts should be shown """ assert isinstance(show, bool) if self.show_disconnected == show: return self.show_disconnected = show self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) def show_empty_groups(self, show): assert isinstance(show, bool) if self._show_empty_groups == show: return self._show_empty_groups = show self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) @property def show_resources(self) -> bool: return self._show_resources @show_resources.setter def show_resources(self, show: bool) -> None: assert isinstance(show, bool) if self._show_resources == show: return self._show_resources = show self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) def plug(self): handler.add_profile(self.profile) def unplug(self): handler.remove_profile(self.profile) def update(self, entities=None, type_=None, profile=None): handler.update(entities, type_, profile) class QuickContactListHandler(object): def __init__(self, host): super(QuickContactListHandler, self).__init__() self.host = host global handler if handler is not None: raise exceptions.InternalError( "QuickContactListHandler must be instanciated only once" ) handler = self self._clist = {} # key: profile, value: ProfileContactList self._widgets = set() self._update_locked = False # se to True to ignore updates def __getitem__(self, profile): """Return ProfileContactList instance for the requested profile""" return self._clist[profile] def __contains__(self, entity): """Check if entity is in contact list @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) """ for contact_list in self._clist.values(): if entity in contact_list: return True return False @property def roster(self): """Return all the bare JIDs of the roster entities. @return (set[jid.JID]) """ entities = set() for contact_list in self._clist.values(): entities.update(contact_list.roster) return entities @property def roster_connected(self): """Return all the bare JIDs of the roster entities that are connected. @return (set[jid.JID]) """ entities = set() for contact_list in self._clist.values(): entities.update(contact_list.roster_connected) return entities @property def roster_entities_by_group(self): """Return a dictionary binding the roster groups to their entities bare JIDs. This also includes the empty group (None key). @return (dict[unicode,set(jid.JID)]) """ groups = {} for contact_list in self._clist.values(): groups.update(contact_list.roster_entities_by_group) return groups @property def roster_groups_by_entities(self): """Return a dictionary binding the entities bare JIDs to their roster groups. @return (dict[jid.JID, set(unicode)]) """ entities = {} for contact_list in self._clist.values(): entities.update(contact_list.roster_groups_by_entities) return entities @property def selected(self): """Return contacts currently selected @return (set): set of selected entities """ entities = set() for contact_list in self._clist.values(): entities.update(contact_list.selected) return entities @property def all_iter(self): """Return item representation for all entities in cache items are unordered """ for profile, contact_list in self._clist.items(): for bare_jid, cache in contact_list.all_iter: data = cache.copy() data[C.CONTACT_PROFILE] = profile yield bare_jid, data @property def items(self): """Return item representation for visible entities in cache items are unordered key: bare jid, value: data """ items = {} for profile, contact_list in self._clist.items(): for bare_jid, cache in contact_list.items.items(): data = cache.copy() items[bare_jid] = data data[C.CONTACT_PROFILE] = profile return items @property def items_sorted(self): """Return item representation for visible entities in cache items are ordered using self.items_sort key: bare jid, value: data """ return self.items_sort(self.items) def items_sort(self, items): """sort items @param items(dict): items to sort (will be emptied !) @return (OrderedDict): sorted items """ ordered_items = OrderedDict() bare_jids = sorted(items.keys()) for jid_ in bare_jids: ordered_items[jid_] = items.pop(jid_) return ordered_items def register(self, widget): """Register a QuickContactList widget This method should only be used in QuickContactList """ self._widgets.add(widget) def unregister(self, widget): """Unregister a QuickContactList widget This method should only be used in QuickContactList """ self._widgets.remove(widget) def add_profiles(self, profiles): """Add a contact list for plugged profiles @param profile(iterable[unicode]): plugged profiles """ for profile in profiles: if profile not in self._clist: self._clist[profile] = ProfileContactList(profile) return [self._clist[profile] for profile in profiles] def add_profile(self, profile): return self.add_profiles([profile])[0] def remove_profiles(self, profiles): """Remove given unplugged profiles from contact list @param profile(iterable[unicode]): unplugged profiles """ for profile in profiles: del self._clist[profile] def remove_profile(self, profile): self.remove_profiles([profile]) def get_special_extras(self, special_type=None): """Return special extras with given type If special_type is None, return all special extras. @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP) None to return all special extras. @return (set[jid.JID]) """ entities = set() for contact_list in self._clist.values(): entities.update(contact_list.get_special_extras(special_type)) return entities def _contacts_filled(self, profile): self._to_fill.remove(profile) if not self._to_fill: del self._to_fill # we need a full update when all contacts are filled self.update() self.host.call_listeners("contactsFilled", profile=profile) def fill(self, profile=None): """Get all contacts from backend, and fill the widget Contacts will be cleared before refilling them @param profile(unicode, None): profile to fill None to fill all profiles """ try: to_fill = self._to_fill except AttributeError: to_fill = self._to_fill = set() # we check if profiles have already been filled # to void filling them several times filled = to_fill.copy() if profile is not None: assert profile in self._clist to_fill.add(profile) else: to_fill.update(list(self._clist.keys())) remaining = to_fill.difference(filled) if remaining != to_fill: log.debug( "Not re-filling already filled contact list(s) for {}".format( ", ".join(to_fill.intersection(filled)) ) ) for profile in remaining: self._clist[profile]._fill() def clear_contacts(self, keep_cache=False): """Clear all the contact list @param keep_cache: if True, don't reset the cache """ for contact_list in self._clist.values(): contact_list.clear_contacts(keep_cache) # we need a full update self.update() def select(self, entity): for contact_list in self._clist.values(): contact_list.select(entity) def unselect(self, entity): for contact_list in self._clist.values(): contact_list.select(entity) def lock_update(self, locked=True, do_update=True): """Forbid contact list updates Used mainly while profiles are plugged, as many updates can occurs, causing an impact on performances @param locked(bool): updates are forbidden if True @param do_update(bool): if True, a full update is done after unlocking if set to False, widget state can be inconsistent, be sure to know what youa re doing! """ log.debug( "Contact lists updates are now {}".format( "LOCKED" if locked else "UNLOCKED" ) ) self._update_locked = locked if not locked and do_update: self.update() def update(self, entities=None, type_=None, profile=None): if not self._update_locked: for widget in self._widgets: widget.update(entities, type_, profile) class QuickContactList(QuickWidget): """This class manage the visual representation of contacts""" SINGLE = False PROFILES_MULTIPLE = True # Can be linked to no profile (e.g. at the early frontend start) PROFILES_ALLOW_NONE = True def __init__(self, host, profiles): super(QuickContactList, self).__init__(host, None, profiles) # options # for next values, None means use indivual value per profile # True or False mean override these values for all profiles self.show_disconnected = None # TODO self._show_empty_groups = None # TODO self._show_resources = None # TODO self.show_status = None # TODO def post_init(self): """Method to be called by frontend after widget is initialised""" handler.register(self) @property def all_iter(self): return handler.all_iter @property def items(self): return handler.items @property def show_resources(self) -> bool|None: return self._show_resources @show_resources.setter def show_resources(self, show: bool|None) -> None: self._show_resources = show @property def items_sorted(self): return handler.items_sorted def update(self, entities=None, type_=None, profile=None): """Update the display when something changed @param entities(iterable[jid.JID], None): updated entities, None to update the whole contact list @param type_(unicode, None): update type, may be: - C.UPDATE_DELETE: entity deleted - C.UPDATE_MODIFY: entity updated - C.UPDATE_ADD: entity added - C.UPDATE_SELECTION: selection modified - C.UPDATE_STRUCTURE: organisation of items is modified (not items themselves) or None for undefined update Note that events correspond to addition, modification and deletion of items on the whole contact list. If the contact is visible or not has no influence on the type_. @param profile(unicode, None): profile concerned with the update None if all profiles need to be updated """ raise NotImplementedError def on_delete(self): QuickWidget.on_delete(self) handler.unregister(self)