Mercurial > libervia-backend
view frontends/src/primitivus/chat.py @ 1095:ef7b7dd5c5db
plugin OTR: various improvments:
- otr_data is now saved between sessions
- private key is encrypted before saving
- state change is detected (unencrypted/started/refreshed/finished) and (un)trusted (trusting is not implemented yet)
- user feedback
- fixed unencrypted message in an encrypted context warning
- fixed inject msg encoding
- minor refactoring
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 26 Jun 2014 14:58:25 +0200 |
parents | 11e2bb20e896 |
children | e2e1e27a3680 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # Primitivus: 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 _ import urwid from urwid_satext import sat_widgets from urwid_satext.files_management import FileDialog from sat_frontends.quick_frontend.quick_chat import QuickChat from sat_frontends.primitivus.card_game import CardGame from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate from sat_frontends.primitivus.xmlui import XMLUI from sat_frontends.primitivus.constants import Const as C import time from sat.tools.jid 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: canvas.set_cursor(self.get_cursor_coords(size)) return canvas def get_cursor_coords(self, size): #(maxcol,) = 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 "*")) else: render_txt.append(('my_nick' if self.my_mess else 'other_nick',"[%s] " % self.nick)) render_txt.append(self.message) return urwid.Text(render_txt, align=self.align) class Chat(urwid.WidgetWrap, QuickChat): def __init__(self, target, host, type_='one2one'): self.target = target QuickChat.__init__(self, target, host, type_) 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]) urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile)) self.setType(type_) 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 == "meta p": #user wants to (un)hide the presents panel if self.type == 'group': widgets = [widget for (widget, options) in self.chat_colums.contents] if self.present_panel in widgets: self.__removePresentPanel() else: self.__appendPresentPanel() elif key == "meta t": #user wants to (un)hide timestamp self.show_timestamp = not self.show_timestamp for wid in self.content: wid._invalidate() elif key == "meta n": #user wants to (not) use short nick self.show_short_nick = not self.show_short_nick for wid in self.content: wid._invalidate() elif key == "meta l": #user wants to (un)hide widget decoration show = not isinstance(self._w, sat_widgets.LabelLine) self.showDecoration(show) self._invalidate() elif key == "meta s": #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 self._invalidate() return super(Chat, self).keypress(size, key) def getMenu(self): """Return Menu bar""" menu = sat_widgets.Menu(self.host.loop) if self.type == 'group': self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) game = _("Game") menu.addMenu(game, "Tarot", self.onTarotRequest) elif self.type == 'one2one': self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target}) menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) return menu def setType(self, type_): QuickChat.setType(self, type_) if type_ == 'one2one': self.historyPrint(profile=self.host.profile) elif type_ == 'group': if len(self.chat_colums.contents) == 1: present_widget = self.__buildPresentList() self.present_panel = sat_widgets.VerticalSeparator(present_widget) self.__appendPresentPanel() def __getDecoration(self, widget): return sat_widgets.LabelLine(widget, self.__getSurrendedText()) def __getSurrendedText(self): """Get the text to be displayed as the dialog title.""" if not hasattr(self, "surrended_text"): self.__setSurrendedText() return self.surrended_text def __setSurrendedText(self, state=None): """Set the text to be displayed as the dialog title @param stat: chat state of the contact """ text = unicode(unescapePrivate(self.target)) if state: text += " (" + state + ")" self.surrended_text = sat_widgets.SurroundedText(text) def showDecoration(self, show=True): """Show/Hide the decoration around the chat window""" if show: main_widget = self.__getDecoration(self.pile) else: main_widget = self.pile self._w = main_widget def updateChatState(self, state, nick=None): """Set the chat state (XEP-0085) of the contact. Leave nick to None to set the state for a one2one conversation, or give a nickname or C.ALL_OCCUPANTS to set the state of a participant within a MUC. @param state: the new chat state @param nick: None for one2one, the MUC user nick or C.ALL_OCCUPANTS """ if nick: assert(self.type == 'group') occupants = self.occupants if nick == C.ALL_OCCUPANTS else [nick] options = self.present_wid.getAllValues() for index in xrange(0, len(options)): nick = options[index].value if nick in occupants: options[index] = (nick, '%s %s' % (C.MUC_USER_STATES[state], nick)) self.present_wid.changeValues(options) self.host.redraw() else: assert(self.type == 'one2one') self.__setSurrendedText(state) self.showDecoration() self.host.redraw() def _presentClicked(self, list_wid, clicked_wid): assert(self.type == 'group') nick = clicked_wid.getValue().value if nick == self.getUserNick(): #We ignore click on our own nick return #we have a click on a nick, we add the private conversation to the contact_list full_jid = JID("%s/%s" % (self.target.bare, nick)) new_jid = escapePrivate(full_jid) if new_jid not in self.host.contact_list: self.host.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER]) #now we select the new window self.host.contact_list.setFocus(full_jid, True) def __buildPresentList(self): self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked) return self.present_wid def __appendPresentPanel(self): self.chat_colums.contents.append((self.present_panel,('weight', 2, False))) def __removePresentPanel(self): for widget, options in self.chat_colums.contents: if widget is self.present_panel: self.chat_colums.contents.remove((widget, options)) break def __appendGamePanel(self, widget): 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)))) self.host.redraw() def __removeGamePanel(self): assert (len(self.pile.contents) == 3) del self.pile.contents[0] self.host.redraw() 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') self.host.redraw() def setPresents(self, param_nicks): """Set the users presents in the contact list for a group chat @param nicks: list of nicknames """ nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge nicks.sort() QuickChat.setPresents(self, nicks) self.present_wid.changeValues(nicks) self.host.redraw() def replaceUser(self, param_nick, show_info=True): """Add user if it is not in the group list""" nick = unicode(param_nick) #FIXME: should be done in DBus bridge QuickChat.replaceUser(self, nick, show_info) presents = self.present_wid.getAllValues() if nick not in [present.value for present in presents]: presents.append(nick) presents.sort(cmp=lambda a, b: cmp(a.value if hasattr(a, 'value') else a, b.value if hasattr(b, 'value') else b)) self.present_wid.changeValues(presents) self.host.redraw() def removeUser(self, param_nick, show_info=True): """Remove a user from the group list""" nick = unicode(param_nick) #FIXME: should be done in DBus bridge QuickChat.removeUser(self, nick, show_info) self.present_wid.deleteValue(nick) self.host.redraw() def printMessage(self, from_jid, msg, profile, timestamp=""): assert isinstance(from_jid, JID) try: jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) except TypeError: return new_text = ChatText(self, timestamp or None, nick, mymess, msg) if timestamp and self.content: for idx in range(len(self.content) - 1, -1, -1): current_text = self.content[idx] if new_text.timestamp < current_text.timestamp and idx > 0: continue # the new message is older, we need to insert it upper #we discard double messages, to avoid backlog / history conflict if ((idx and self.content[idx - 1].message == msg) or (self.content[idx].message == msg) or (idx < len(self.content) - 2 and self.content[idx + 1].message)): return self.content.insert(idx + 1, new_text) break else: self.content.append(new_text) self._notify(from_jid, msg) def printInfo(self, msg, type_='normal', timestamp=""): """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" """ _widget = ChatText(self, timestamp or None, None, False, msg, is_info=True) self.content.append(_widget) self._notify(msg=msg) def _notify(self, from_jid="somebody", msg=""): """Notify the user of a new message if primitivus doesn't have the focus. @param from_jid: contact who wrote to the users @param msg: the message that has been received """ if msg == "": return 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 self.host.redraw() if not self.host.x_notify.hasFocus(): if self.type == "one2one": self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid) elif self.getUserNick().lower() in msg.lower(): self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target}) def startGame(self, game_type, referee, players): """Configure the chat window to start a game""" if game_type=="Tarot": self.tarot_wid = CardGame(self, referee, players, self.nick) self.__appendGamePanel(self.tarot_wid) def getGame(self, game_type): """Return class managing the game type""" #TODO: check that the game is launched, and manage errors if game_type=="Tarot": return self.tarot_wid #MENU EVENTS# def onTarotRequest(self, menu): # TODO: move this to plugin_misc_tarot with dynamic menu if len(self.occupants) != 4: self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp)) else: self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile) def onConfigureRoom(self, menu): def gotUI(xmlui): self.host.addWindow(XMLUI(self.host, xmlui)) def configureError(failure): self.host.showPopUp(sat_widgets.Alert(failure.fullname, failure.message, ok_cb=self.host.removePopUp)) self.host.bridge.configureRoom(self.id, self.host.profile, callback=gotUI, errback=configureError) def onSendFileRequest(self, menu): # TODO: move this to core with dynamic menus dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) self.host.showPopUp(dialog, 80, 80) #MISC EVENTS# def onFileSelected(self, filepath): self.host.removePopUp() #FIXME: check last_resource: what if self.target.resource exists ? last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile) if last_resource: full_jid = JID("%s/%s" % (self.target.bare, last_resource)) else: full_jid = self.target progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile) self.host.addProgress(progress_id,filepath)