view sat/stdui/ui_contact_list.py @ 3254:6cf4bd6972c2

core, frontends: avatar refactoring: /!\ huge commit Avatar logic has been reworked around the IDENTITY plugin: plugins able to handle avatar or other identity related metadata (like nicknames) register to IDENTITY plugin in the same way as for other features like download/upload. Once registered, IDENTITY plugin will call them when suitable in order of priority, and handle caching. Methods to manage those metadata from frontend now use serialised data. For now `avatar` and `nicknames` are handled: - `avatar` is now a dict with `path` + metadata like `media_type`, instead of just a string path - `nicknames` is now a list of nicknames in order of priority. This list is never empty, and `nicknames[0]` should be the preferred nickname to use by frontends in most cases. In addition to contact specified nicknames, user set nickname (the one set in roster) is used in priority when available. Among the side changes done with this commit, there are: - a new `contactGet` bridge method to get roster metadata for a single contact - SatPresenceProtocol.send returns a Deferred to check when it has actually been sent - memory's methods to handle entities data now use `client` as first argument - metadata filter can be specified with `getIdentity` - `getAvatar` and `setAvatar` are now part of the IDENTITY plugin instead of XEP-0054 (and there signature has changed) - `isRoom` and `getBareOrFull` are now part of XEP-0045 plugin - jp avatar/get command uses `xdg-open` first when available for `--show` flag - `--no-cache` has been added to jp avatar/get and identity/get - jp identity/set has been simplified, explicit options (`--nickname` only for now) are used instead of `--field`. `--field` may come back in the future if necessary for extra data. - QuickContactList `SetContact` now handle None as a value, and doesn't use it to delete the metadata anymore - improved cache handling for `metadata` and `nicknames` in quick frontend - new `default` argument in QuickContactList `getCache`
author Goffi <goffi@goffi.org>
date Tue, 14 Apr 2020 21:00:33 +0200
parents 559a625a236b
children be6d91572633
line wrap: on
line source

#!/usr/bin/env python3


# SAT standard user interface for managing contacts
# Copyright (C) 2009-2020 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 _, D_
from sat.core.constants import Const as C
from sat.tools import xml_tools
from twisted.words.protocols.jabber import jid
from xml.dom.minidom import Element


class ContactList(object):
    """Add, update and remove contacts."""

    def __init__(self, host):
        self.host = host
        self.__add_id = host.registerCallback(self._addContact, with_data=True)
        self.__update_id = host.registerCallback(self._updateContact, with_data=True)
        self.__confirm_delete_id = host.registerCallback(
            self._getConfirmRemoveXMLUI, with_data=True
        )

        host.importMenu(
            (D_("Contacts"), D_("Add contact")),
            self._getAddDialogXMLUI,
            security_limit=2,
            help_string=D_("Add contact"),
        )
        host.importMenu(
            (D_("Contacts"), D_("Update contact")),
            self._getUpdateDialogXMLUI,
            security_limit=2,
            help_string=D_("Update contact"),
        )
        host.importMenu(
            (D_("Contacts"), D_("Remove contact")),
            self._getRemoveDialogXMLUI,
            security_limit=2,
            help_string=D_("Remove contact"),
        )

        # FIXME: a plugin should not be used here, and current profile's jid host would be better than installation wise host
        if "MISC-ACCOUNT" in self.host.plugins:
            self.default_host = self.host.plugins["MISC-ACCOUNT"].getNewAccountDomain()
        else:
            self.default_host = "example.net"

    def getContacts(self, profile):
        """Return a sorted list of the contacts for that profile

        @param profile: %(doc_profile)s
        @return: list[string]
        """
        client = self.host.getClient(profile)
        ret = [contact.full() for contact in client.roster.getJids()]
        ret.sort()
        return ret

    def getGroups(self, new_groups=None, profile=C.PROF_KEY_NONE):
        """Return a sorted list of the groups for that profile

        @param new_group (list): add these groups to the existing ones
        @param profile: %(doc_profile)s
        @return: list[string]
        """
        client = self.host.getClient(profile)
        ret = client.roster.getGroups()
        ret.sort()
        ret.extend([group for group in new_groups if group not in ret])
        return ret

    def getGroupsOfContact(self, user_jid_s, profile):
        """Return all the groups of the given contact

        @param user_jid_s (string)
        @param profile: %(doc_profile)s
        @return: list[string]
        """
        client = self.host.getClient(profile)
        return client.roster.getItem(jid.JID(user_jid_s)).groups

    def getGroupsOfAllContacts(self, profile):
        """Return a mapping between the contacts and their groups

        @param profile: %(doc_profile)s
        @return: dict (key: string, value: list[string]):
            - key: the JID userhost
            - value: list of groups
        """
        client = self.host.getClient(profile)
        return {item.jid.userhost(): item.groups for item in client.roster.getItems()}

    def _data2elts(self, data):
        """Convert a contacts data dict to minidom Elements

        @param data (dict)
        @return list[Element]
        """
        elts = []
        for key in data:
            key_elt = Element("jid")
            key_elt.setAttribute("name", key)
            for value in data[key]:
                value_elt = Element("group")
                value_elt.setAttribute("name", value)
                key_elt.childNodes.append(value_elt)
            elts.append(key_elt)
        return elts

    def getDialogXMLUI(self, options, data, profile):
        """Generic method to return the XMLUI dialog for adding or updating a contact

        @param options (dict): parameters for the dialog, with the keys:
                               - 'id': the menu callback id
                               - 'title': deferred localized string
                               - 'contact_text': deferred localized string
        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        form_ui = xml_tools.XMLUI("form", title=options["title"], submit_id=options["id"])
        if "message" in data:
            form_ui.addText(data["message"])
            form_ui.addDivider("dash")

        form_ui.addText(options["contact_text"])
        if options["id"] == self.__add_id:
            contact = data.get(
                xml_tools.formEscape("contact_jid"), "@%s" % self.default_host
            )
            form_ui.addString("contact_jid", value=contact)
        elif options["id"] == self.__update_id:
            contacts = self.getContacts(profile)
            list_ = form_ui.addList("contact_jid", options=contacts, selected=contacts[0])
            elts = self._data2elts(self.getGroupsOfAllContacts(profile))
            list_.setInternalCallback(
                "groups_of_contact", fields=["contact_jid", "groups_list"], data_elts=elts
            )

        form_ui.addDivider("blank")

        form_ui.addText(_("Select in which groups your contact is:"))
        selected_groups = []
        if "selected_groups" in data:
            selected_groups = data["selected_groups"]
        elif options["id"] == self.__update_id:
            try:
                selected_groups = self.getGroupsOfContact(contacts[0], profile)
            except IndexError:
                pass
        groups = self.getGroups(selected_groups, profile)
        form_ui.addList(
            "groups_list", options=groups, selected=selected_groups, styles=["multi"]
        )

        adv_list = form_ui.changeContainer("advanced_list", columns=3, selectable="no")
        form_ui.addLabel(D_("Add group"))
        form_ui.addString("add_group")
        button = form_ui.addButton("", value=D_("Add"))
        button.setInternalCallback("move", fields=["add_group", "groups_list"])
        adv_list.end()

        form_ui.addDivider("blank")
        return {"xmlui": form_ui.toXml()}

    def _getAddDialogXMLUI(self, data, profile):
        """Get the dialog for adding contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        options = {
            "id": self.__add_id,
            "title": D_("Add contact"),
            "contact_text": D_("New contact identifier (JID):"),
        }
        return self.getDialogXMLUI(options, {}, profile)

    def _getUpdateDialogXMLUI(self, data, profile):
        """Get the dialog for updating contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        if not self.getContacts(profile):
            _dialog = xml_tools.XMLUI("popup", title=D_("Nothing to update"))
            _dialog.addText(_("Your contact list is empty."))
            return {"xmlui": _dialog.toXml()}

        options = {
            "id": self.__update_id,
            "title": D_("Update contact"),
            "contact_text": D_("Which contact do you want to update?"),
        }
        return self.getDialogXMLUI(options, {}, profile)

    def _getRemoveDialogXMLUI(self, data, profile):
        """Get the dialog for removing contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        if not self.getContacts(profile):
            _dialog = xml_tools.XMLUI("popup", title=D_("Nothing to delete"))
            _dialog.addText(_("Your contact list is empty."))
            return {"xmlui": _dialog.toXml()}

        form_ui = xml_tools.XMLUI(
            "form",
            title=D_("Who do you want to remove from your contacts?"),
            submit_id=self.__confirm_delete_id,
        )
        form_ui.addList("contact_jid", options=self.getContacts(profile))
        return {"xmlui": form_ui.toXml()}

    def _getConfirmRemoveXMLUI(self, data, profile):
        """Get the confirmation dialog for removing contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        if C.bool(data.get("cancelled", "false")):
            return {}
        contact = data[xml_tools.formEscape("contact_jid")]

        def delete_cb(data, profile):
            if not C.bool(data.get("cancelled", "false")):
                self._deleteContact(jid.JID(contact), profile)
            return {}

        delete_id = self.host.registerCallback(delete_cb, with_data=True, one_shot=True)
        form_ui = xml_tools.XMLUI("form", title=D_("Delete contact"), submit_id=delete_id)
        form_ui.addText(
            D_("Are you sure you want to remove %s from your contact list?") % contact
        )
        return {"xmlui": form_ui.toXml()}

    def _addContact(self, data, profile):
        """Add the selected contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        if C.bool(data.get("cancelled", "false")):
            return {}
        contact_jid_s = data[xml_tools.formEscape("contact_jid")]
        try:
            contact_jid = jid.JID(contact_jid_s)
        except (RuntimeError, jid.InvalidFormat, AttributeError):
            # TODO: replace '\t' by a constant (see tools.xmlui.XMLUI.onFormSubmitted)
            data["selected_groups"] = data[xml_tools.formEscape("groups_list")].split(
                "\t"
            )
            options = {
                "id": self.__add_id,
                "title": D_("Add contact"),
                "contact_text": D_('Please enter a valid JID (like "contact@%s"):')
                % self.default_host,
            }
            return self.getDialogXMLUI(options, data, profile)
        self.host.addContact(contact_jid, profile_key=profile)
        return self._updateContact(data, profile)  # after adding, updating

    def _updateContact(self, data, profile):
        """Update the selected contact

        @param data (dict)
        @param profile: %(doc_profile)s
        @return dict
        """
        client = self.host.getClient(profile)
        if C.bool(data.get("cancelled", "false")):
            return {}
        contact_jid = jid.JID(data[xml_tools.formEscape("contact_jid")])
        # TODO: replace '\t' by a constant (see tools.xmlui.XMLUI.onFormSubmitted)
        groups = data[xml_tools.formEscape("groups_list")].split("\t")
        self.host.updateContact(client, contact_jid, name="", groups=groups)
        return {}

    def _deleteContact(self, contact_jid, profile):
        """Delete the selected contact

        @param contact_jid (JID)
        @param profile: %(doc_profile)s
        @return dict
        """
        self.host.delContact(contact_jid, profile_key=profile)
        return {}