view frontends/src/quick_frontend/quick_chat.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents f0c9b149ed99
children faa1129559b8
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.tools.jid  import JID
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.quick_frontend.constants import Const as C


class QuickChat(quick_widgets.QuickWidget):

    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
        """
        @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
        """

        quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
        assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
        if type_ == C.CHAT_GROUP and target.resource:
            raise ValueError("A group chat entity can't have a resource")
        self.current_target = target
        self.type = type_
        self.id = "" # FIXME: to be removed
        self.nick = None
        self.occupants = set()

    def __str__(self):
        return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)

    @staticmethod
    def getWidgetHash(target, profile):
        return (unicode(profile), target.bare)

    @staticmethod
    def getPrivateHash(target, profile):
        """Get unique hash for private conversations

        This method should be used with force_hash to get unique widget for private MUC conversations
        """
        return (unicode(profile), target)


    def addTarget(self, target):
        super(QuickChat, self).addTarget(target)
        if target.resource:
            self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead

    @property
    def target(self):
        if self.type == C.CHAT_GROUP:
            return self.current_target.bare
        return self.current_target

    def manageMessage(self, entity, mess_type):
        """Tell if this chat widget manage an entity and message type couple

        @param entity (jid.JID): (full) jid of the sending entity
        @param mess_type (str): message type as given by newMessage
        @return (bool): True if this Chat Widget manage this couple
        """
        if self.type == C.CHAT_GROUP:
            if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
                return True
        else:
            if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
                return True
        return False

    def setPresents(self, nicks):
        """Set the users presents in the contact list for a group chat
        @param nicks: list of nicknames
        """
        log.debug (_("Adding users %s to room") % nicks)
        if self.type != C.CHAT_GROUP:
            log.error (_("[INTERNAL] trying to set presents nicks for a non group chat window"))
            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
        self.occupants.update(nicks)

    def replaceUser(self, nick, show_info=True):
        """Add user if it is not in the group list"""
        log.debug (_("Replacing user %s") % nick)
        if self.type != C.CHAT_GROUP:
            log.error (_("[INTERNAL] trying to replace user for a non group chat window"))
            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
        len_before = len(self.occupants)
        self.occupants.add(nick)
        if len_before != len(self.occupants) and show_info:
            self.printInfo("=> %s has joined the room" % nick)

    def removeUser(self, nick, show_info=True):
        """Remove a user from the group list"""
        log.debug(_("Removing user %s") % nick)
        if self.type != C.CHAT_GROUP:
            log.error (_("[INTERNAL] trying to remove user for a non group chat window"))
            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
        self.occupants.remove(nick)
        if show_info:
            self.printInfo("<= %s has left the room" % nick)

    def setUserNick(self, nick):
        """Set the nick of the user, usefull for e.g. change the color of the user"""
        self.nick = nick

    def getUserNick(self):
        return unicode(self.nick)

    def changeUserNick(self, old_nick, new_nick):
        """Change nick of a user in group list"""
        log.debug(_("Changing nick of user %(old_nick)s to %(new_nick)s") % {"old_nick": old_nick, "new_nick": new_nick})
        if self.type != C.CHAT_GROUP:
            log.error (_("[INTERNAL] trying to change user nick for a non group chat window"))
            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
        self.removeUser(old_nick, show_info=False)
        self.replaceUser(new_nick, show_info=False)
        self.printInfo("%s is now known as %s" % (old_nick, new_nick))

    def setSubject(self, subject):
        """Set title for a group chat"""
        log.debug(_("Setting subject to %s") % subject)
        if self.type != C.CHAT_GROUP:
            log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here

    def afterHistoryPrint(self):
        """Refresh or scroll down the focus after the history is printed"""
        pass

    def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
        """Print the current history
        @param size (int): number of messages
        @param search (str): pattern to filter the history results
        @param profile (str): %(doc_profile)s
        """
        log.debug(_("now we print the history (%d messages)") % size)

        def onHistory(history):
            for line in history:
                timestamp, from_jid, to_jid, message, _type, extra = line
                if ((self.type == C.CHAT_GROUP and _type != C.MESS_TYPE_GROUPCHAT) or
                   (self.type == C.CHAT_ONE2ONE and _type == C.MESS_TYPE_GROUPCHAT)):
                    continue
                self.printMessage(JID(from_jid), message, profile, timestamp)
            self.afterHistoryPrint()

        def onHistoryError(err):
            log.error(_("Can't get history"))

        target = self.target.bare

        return self.host.bridge.getHistory(self.host.profiles[profile].whoami.bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)

    def _get_nick(self, entity):
        """Return nick of this entity when possible"""
        if self.type == C.CHAT_GROUP:
            return entity.resource
        contact_list = self.host.contact_lists[self.profile]
        if entity.bare in contact_list:
            return contact_list.getCache(entity,'nick') or contact_list.getCache(entity,'name') or entity.node or entity
        return entity.node or entity

    def onPrivateCreated(self, widget):
        """Method called when a new widget for private conversation (MUC) is created"""
        raise NotImplementedError

    def getOrCreatePrivateWidget(self, entity):
        """Create a widget for private conversation, or get it if it already exists

        @param entity: full jid of the target
        """
        return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again

    def newMessage(self, from_jid, target, msg, type_, extra, profile):
        if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
            # we have a private message, we forward it to a private conversation widget
            chat_widget = self.getOrCreatePrivateWidget(target)
            chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
        else:
            timestamp = extra.get('archive')
            if type_ == C.MESS_TYPE_INFO:
                self.printInfo(msg, timestamp=float(timestamp) if timestamp else None)
            else:
                self.printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)

    def printMessage(self, from_jid, msg, profile, timestamp=None):
        """Print message in chat window. Must be implemented by child class"""
        jid = JID(from_jid)
        nick = self._get_nick(jid)
        mymess = (jid.resource == self.nick) if self.type == C.CHAT_GROUP else (jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
        if msg.startswith('/me '):
            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp)
            return
        return jid, nick, mymess

    def printInfo(self, msg, type_='normal', timestamp=None):
        """Print general info
        @param msg: message to print
        @type_: one of:
            normal: general info like "toto has joined the room"
            me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
        @param timestamp (float): number of seconds since epoch
        """
        raise NotImplementedError

    def startGame(self, game_type, referee, players):
        """Configure the chat window to start a game"""
        #No need to raise an error as game are not mandatory
        log.warning(_('startGame is not implemented in this frontend'))

    def getGame(self, game_type):
        """Return class managing the game type"""
        #No need to raise an error as game are not mandatory
        log.warning(_('getGame is not implemented in this frontend'))

    def updateChatState(self, from_jid, state):
        """Set the chat state (XEP-0085) of the contact.

        @param state: the new chat state
        """
        raise NotImplementedError

quick_widgets.register(QuickChat)