view frontends/src/primitivus/ @ 1751:77870d2e2902

primitivus (chat): duplicate message check is actually not needed when retrieving local MUC history is disabled
author souliane <>
date Wed, 16 Dec 2015 13:29:24 +0100
parents 3a6cd1c14974
children 2e2fb462729a
line wrap: on
line source

# -*- coding: utf-8 -*-

# Primitivus: a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (

# 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
# 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 <>.

from sat.core.i18n import _
from sat.core import log as logging
log = logging.getLogger(__name__)
import urwid
from urwid_satext import sat_widgets
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.quick_frontend.quick_chat import QuickChat
from sat_frontends.quick_frontend import quick_games
from sat_frontends.primitivus import game_tarot
from sat_frontends.primitivus.constants import Const as C
from sat_frontends.primitivus.keys import action_key_map as a_key
from sat_frontends.primitivus.widget import PrimitivusWidget
import time
from import jid

class ChatText(urwid.FlowWidget):
    """Manage the printing of chat message"""

    def __init__(self, parent, timestamp, nick, my_mess, message, align='left', is_info=False):
        self.parent = parent
        self.timestamp = time.localtime(timestamp)
        self.nick = nick
        self.my_mess = my_mess
        self.message = unicode(message)
        self.align = align
        self.is_info = is_info

    def selectable(self):
        return True

    def keypress(self, size, key):
        return key

    def rows(self, size, focus=False):
        return self.display_widget(size, focus).rows(size, focus)

    def render(self, size, focus=False):
        canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus))
        if focus:
        return canvas

    def get_cursor_coords(self, size):
        return 0, 0

    def display_widget(self, size, focus):
        render_txt = []
        if not self.is_info:
            if self.parent.show_timestamp:
                time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M"  # if the message was sent before today, we print the full date
                render_txt.append(('date', "[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8')))
            if self.parent.show_short_nick:
                render_txt.append(('my_nick' if self.my_mess else 'other_nick', "**" if self.my_mess else "*"))
                render_txt.append(('my_nick' if self.my_mess else 'other_nick', "[%s] " % (self.nick or '')))
        txt_widget = urwid.Text(render_txt, align=self.align)
        if self.is_info:
            return urwid.AttrMap(txt_widget, 'info_msg')
        return txt_widget

class Chat(PrimitivusWidget, QuickChat):

    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
        QuickChat.__init__(self, host, target, type_, profiles=profiles)
        self.content = urwid.SimpleListWalker([])
        self.text_list = urwid.ListBox(self.content)
        self.chat_widget = urwid.Frame(self.text_list)
        self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
        self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
        self.pile = urwid.Pile([self.chat_colums])
        PrimitivusWidget.__init__(self, self.pile,

        # we must adapt the behaviour with the type
        if type_ == C.CHAT_ONE2ONE:
        elif type_ == C.CHAT_GROUP:
            if len(self.chat_colums.contents) == 1:
                self.occupants_list = sat_widgets.GenericList([], option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
                self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_list)
      'presence', self.presenceListener, [profiles])

        self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00  %Y"))  # struct_time of day changing time
        self.show_timestamp = True
        self.show_short_nick = False
        self.show_title = 1  # 0: clip title; 1: full title; 2: no title
        self.subject = None

    def keypress(self, size, key):
        if key == a_key['OCCUPANTS_HIDE']:  # user wants to (un)hide the occupants panel
            if self.type == C.CHAT_GROUP:
                widgets = [widget for (widget, options) in self.chat_colums.contents]
                if self.occupants_panel in widgets:
        elif key == a_key['TIMESTAMP_HIDE']:  # user wants to (un)hide timestamp
            self.show_timestamp = not self.show_timestamp
            for wid in self.content:
        elif key == a_key['SHORT_NICKNAME']:  # user wants to (not) use short nick
            self.show_short_nick = not self.show_short_nick
            for wid in self.content:
        elif key == a_key['SUBJECT_SWITCH']:  # user wants to (un)hide group's subject or change its apperance
            if self.subject:
                self.show_title = (self.show_title + 1) % 3
                if self.show_title == 0:
                    self.setSubject(self.subject, 'clip')
                elif self.show_title == 1:
                    self.setSubject(self.subject, 'space')
                elif self.show_title == 2:
                    self.chat_widget.header = None

        return super(Chat, self).keypress(size, key)

    def getMenu(self):
        """Return Menu bar"""
        menu = sat_widgets.Menu(
        if self.type == C.CHAT_GROUP:
  , C.MENU_ROOM, {'room_jid':})
            game = _("Game")
            menu.addMenu(game, "Tarot", self.onTarotRequest)
        elif self.type == C.CHAT_ONE2ONE:
            # FIXME: is a bare jid, we need to check that
            contact_list =[self.profile]
            if not
                full_jid = contact_list.getFullJid(
                full_jid =
  , C.MENU_SINGLE, {'jid': full_jid})
        return menu

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

        @param entity (jid.JID): entity updated
        @param show: availability
        @param priority: resource's priority
        @param statuses: dict of statuses
        @param profile: %(doc_profile)s
        assert self.type == C.CHAT_GROUP
        if entity.bare !=

    def update(self, entity=None):
        """Update one or all entities.

        @param entity (jid.JID): entity to update
        contact_list =[self.profile]

        if self.type == C.CHAT_ONE2ONE:  # only update the chat title
            states = self.getEntityStates(
            self.title_dynamic = ' '.join([u'({})'.format(state) for state in states.values()])

        nicks = list(self.occupants)
        if entity is None:  # rebuild all the occupants list
            values = []
            for nick in nicks:
                values.append(self._buildOccupantMarkup(jid.newResource(, nick)))
        else:  # add, remove or update only one occupant
            nick = entity.resource
            show = contact_list.getCache(entity, C.PRESENCE_SHOW)
            if show == C.PRESENCE_UNAVAILABLE or show is None:
                except ValueError:
                values = self.occupants_list.getAllValues()
                markup = self._buildOccupantMarkup(entity)
                if not values:  # room has just been created
                    values = [markup]
                else:  # add or update the occupant, keep the list sorted
                    index = 0
                    for entry in values:
                        order = cmp(entry.value if hasattr(entry, 'value') else entry, nick)
                        if order < 0:
                            index += 1
                        if order > 0:  # insert the occupant
                            values.insert(index, markup)
                        else:  # update an existing occupant
                            values[index] = markup
                    if index == len(values):  # add to the end of the list

    def _buildOccupantMarkup(self, entity):
        """Return the option attributes for a MUC occupant.

        @param nick (unicode): occupant nickname
        # TODO: for now it's not a markup but a simple text, the problem is that ListOption is unicode and not urwid.Text
        contact_list =[self.profile]
        show = contact_list.getCache(entity, C.PRESENCE_SHOW)
        states = self.getEntityStates(entity)
        nick = entity.resource
        show_icon, entity_attr = C.PRESENCE.get(show, (u'', u'default'))  # TODO: use entity_attr and return (nick, markup)
        text = "%s%s %s" % (u''.join(states.values()), show_icon, nick)
        return (nick, text)

    def _occupantsClicked(self, list_wid, clicked_wid):
        assert self.type == C.CHAT_GROUP
        nick = clicked_wid.getValue().value
        if nick == self.nick:
            # We ignore clicks on our own nick
        contact_list =[self.profile]
        full_jid = jid.JID("%s/%s" % (, nick))

        # we have a click on a nick, we need to create the widget if it doesn't exists

        # now we select the new window
        contact_list.setFocus(full_jid, True)

    def _appendOccupantsPanel(self):
        self.chat_colums.contents.append((self.occupants_panel, ('weight', 2, False)))

    def _removeOccupantsPanel(self):
        for widget, options in self.chat_colums.contents:
            if widget is self.occupants_panel:
                self.chat_colums.contents.remove((widget, options))

    def addGamePanel(self, widget):
        """Insert a game panel to this Chat dialog.

        @param widget (Widget): the game panel
        assert (len(self.pile.contents) == 1)
        self.pile.contents.insert(0, (widget, ('weight', 1)))
        self.pile.contents.insert(1, (urwid.Filler(urwid.Divider('-'), ('fixed', 1))))

    def removeGamePanel(self, widget):
        """Remove the game panel from this Chat dialog.

        @param widget (Widget): the game panel
        assert (len(self.pile.contents) == 3)
        del self.pile.contents[0]

    def setSubject(self, subject, wrap='space'):
        """Set title for a group chat"""
        QuickChat.setSubject(self, subject)
        self.subject = subject
        self.subj_wid = urwid.Text(unicode(subject.replace('\n', '|') if wrap == 'clip' else subject),
                                   align='left' if wrap == 'clip' else 'center', wrap=wrap)
        self.chat_widget.header = urwid.AttrMap(self.subj_wid, 'title')

    def clearHistory(self):
        """Clear the content of this chat."""
        del self.content[:]

    def afterHistoryPrint(self):
        """Refresh or scroll down the focus after the history is printed"""
        if len(self.content):
            self.text_list.focus_position = len(self.content) - 1  # scroll down

    def onPrivateCreated(self, widget):[widget.profile].specialResourceVisible(

    def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
        """Print message in chat window.

        @param nick (unicode): author nick
        @param my_message (boolean): True if profile is the author
        @param message (unicode): message content
        @param extra (dict): extra data
        new_text = ChatText(self, timestamp, nick, my_message, message)
        if not timestamp:
            # XXX: do not send notifications for each line of the history being displayed
            # FIXME: this must be changed in the future if the timestamp is passed with
            # all messages and not only with the messages coming from the history.
            self._notify(nick, message)

    def printInfo(self, msg, type_='normal', extra=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
        if extra is None:
            extra = {}
            timestamp = float(extra['timestamp'])
        except KeyError:
            timestamp = None
        _widget = ChatText(self, timestamp, None, False, msg, is_info=True)

    def _notify(self, contact="somebody", msg=""):
        """Notify the user of a new message if primitivus doesn't have the focus.

        @param contact (unicode): contact who wrote to the users
        @param msg (unicode): the message that has been received
        if msg == "":
        if self.text_list.get_focus()[1] == len(self.content) - 2:
            # we don't change focus if user is not at the bottom
            # as that mean that he is probably watching discussion history
            self.text_list.focus_position = len(self.content) - 1
        if not
            if self.type == C.CHAT_ONE2ONE:
      "Primitivus: %s is talking to you") % contact)
            elif self.nick is not None and self.nick.lower() in msg.lower():
      "Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': contact, 'room':})

    def onTarotRequest(self, menu):
        # TODO: move this to plugin_misc_tarot with dynamic menu
        if len(self.occupants) != 4:
  "Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"),
  , list(self.occupants), self.profile)


    def onDelete(self):
        if self.type == C.CHAT_GROUP:
  'presence', self.presenceListener)

quick_widgets.register(QuickChat, Chat)
quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)