view frontends/src/quick_frontend/quick_contact_list.py @ 1293:0541cb64217e frontends_multi_profiles

plugin XEP-0054: couple of fixes in VCard/avatar management: - fixed a confusion between full jid and bare jid, resulting in always requesting VCard - check of cache data loading before starting - stored cache are restored for all jid, not only ones in roster - client jid cache is correctly saved/restored - minor other fixes This plugin still need some work, but this patch fixes (hopefully) the major issues
author Goffi <goffi@goffi.org>
date Mon, 26 Jan 2015 02:03:16 +0100
parents faa1129559b8
children ef7e8e23b353 447d28b1b4ec
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# helper class for making a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>.

from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.quick_frontend.quick_widgets import QuickWidget
from sat_frontends.quick_frontend.constants import Const as C


try:
    # FIXME: to be removed when an acceptable solution is here
    unicode('') # 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)


class QuickContactList(QuickWidget):
    """This class manage the visual representation of contacts"""

    def __init__(self, host, profile):
        log.debug(_("Contact List init"))
        super(QuickContactList, self).__init__(host, profile, profile)
        # bare jids as keys, resources are used in data
        self._cache = {}

        # special entities (groupchat, gateways, etc), bare jids
        self._specials = set()
        # extras are specials with full jids (e.g.: private MUC conversation)
        self._special_extras = set()

        # group data contain jids in groups and misc frontend data
        self._groups = {} # groups to group data map

        # contacts in roster (bare jids)
        self._roster = set()

        # entities with an alert (usually a waiting message), full jid
        self._alerts = 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
        # TODO: this may lead to two successive UI refresh and needs an optimization
        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self.showEmptyGroups)
        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self.showOfflineContacts)

    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)
        """
        if entity.resource:
            try:
                return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
            except KeyError:
                return False
        return entity in self._cache

    def fill(self):
        """Get all contacts from backend, and fill the widget"""
        def gotContacts(contacts):
            for contact in contacts:
                self.host.newContactHandler(*contact, profile=self.profile)

        self.host.bridge.getContacts(self.profile, callback=gotContacts)

    def update(self):
        """Update the display when something changed"""
        raise NotImplementedError

    def getCache(self, entity, name=None):
        """Return a cache value for a contact

        @param entity(entity.entity): 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
        """
        try:
            cache = self._cache[entity.bare]
        except KeyError:
            self.setContact(entity)
            cache = self._cache[entity.bare]

        if name is None:
            return cache
        try:
            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]
                    cache = cache[C.CONTACT_RESOURCES][main_resource]
                else:
                    cache = cache[C.CONTACT_RESOURCES][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('default','')

            return cache[name]
        except KeyError:
            return None

    def setCache(self, entity, name, value):
        """Set or update value for one data in cache

        @param entity(JID): entity to update
        @param name(unicode): value to set or update
        """
        self.setContact(entity, None, {name: value})

    def setGroupData(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 is not 'jids'
        self._groups[group][name] = value

    def getGroupData(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 setSpecial(self, entity, special_type):
        """Set special flag on an entity

        @param entity(jid.JID): jid of the special entity
        @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.setCache(entity, C.CONTACT_SPECIAL, special_type)

    def clearContacts(self):
        """Clear all the contact list"""
        self.unselectAll()
        self._cache.clear()
        self._groups.clear()
        self._specials.clear()
        self.update()

    def setContact(self, entity, groups=None, attributes=None, in_roster=False):
        """Add a contact to the list if 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. nickname). 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
        @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

        if in_roster:
            self._roster.add(entity_bare)

        cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})

        assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes

        # we set groups and fill self._groups accordingly
        if groups is not None:
            if not groups:
                groups = [None]  # [None] is the default group
            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_bare)
            else:
                self._specials.add(entity_bare)

        # now the attribute we keep in cache
        for attribute, value in attributes.iteritems():
            cache[attribute] = value

        # we can update the display
        self.update()

    def getContacts(self):
        """Return contacts currently selected

        @return (set): set of selected entities"""
        return self._selected

    def entityToShow(self, entity, check_resource=False):
        """Tell if the contact should be showed or hidden.

        @param contact (jid.JID): jid of the contact
        @param check_resource (bool): True if resource must be significant
        @return: True if that contact should be showed in the list
        """
        show = self.getCache(entity, C.PRESENCE_SHOW)

        if check_resource:
            alerts = self._alerts
            selected = self._selected
        else:
            alerts = {alert.bare for alert in self._alerts}
            selected = {selected.bare for selected in self._selected}
        return ((show is not None and show != "unavailable")
                or self.show_disconnected
                or entity in alerts
                or entity in selected)

    def anyEntityToShow(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
        """
        for entity in entities:
            if self.entityToShow(entity, check_resources):
                return True
        return False

    def remove(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
        try:
            groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
        except KeyError:
            log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
        del self._cache[entity_bare]
        for group in groups:
            self._groups[group]['jids'].remove(entity_bare)
        for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
            to_remove = set()
            for set_entity in set_:
                if set_entity.bare == entity.bare:
                    to_remove.add(set_entity)
            set_.difference_update(to_remove)
        self.update()

    def updatePresence(self, entity, show, priority, statuses):
        """Update entity's presence status

        @param entity(jid.JID): entity to update's entity
        @param show: availability
        @parap priority: resource's priority
        @param statuses: dict of statuses
        """
        cache = self.getCache(entity)
        if show == C.PRESENCE_UNAVAILABLE:
            if not entity.resource:
                cache[C.CONTACT_RESOURCES].clear()
                cache[C.CONTACT_MAIN_RESOURCE]= None
            else:
                del cache[C.CONTACT_RESOURCES][entity.resource]
                if not cache[C.CONTACT_RESOURCES]:
                    cache[C.CONTACT_MAIN_RESOURCE] = None
        else:
            assert entity.resource
            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

            priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
            cache[C.CONTACT_MAIN_RESOURCE] = priority_resource

    def unselectAll(self):
        """Unselect all contacts"""
        self._selected.clear()
        self.update()

    def select(self, entity):
        """Select an entity

        @param entity(jid.JID): entity to select (resource is significant)
        """
        log.debug("select %s" % entity)
        self._selected.add(entity)
        self.update()

    def setAlert(self, entity):
        """Set an alert on the entity (usually for a waiting message)

        @param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
        """
        self._alerts.add(entity)
        self.update()

    def showOfflineContacts(self, show):
        show = C.bool(show)
        if self.show_disconnected == show:
            return
        self.show_disconnected = show
        self.update()

    def showEmptyGroups(self, show):
        show = C.bool(show)
        if self.show_empty_groups == show:
            return
        self.show_empty_groups = show
        self.update()

    def showResources(self, show):
        show = C.bool(show)
        if self.show_resources == show:
            return
        self.show_resources = show
        self.update()