# -*- coding: utf-8 -*- # This file is part of emesene. # # Emesene is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # emesene 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with emesene; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gtk import pango import gobject import time from urllib import quote import emesenelib.common from emesenecommon import MAX_MESSAGE_LENGTH import ConversationUI import Dialogs import Log class CustomEmoticons( object ): # FIXME: wtf? def __init__( self ): self.emoticons = {} def setNew( self, user, shortcut, id ): if user != None: user = user.lower() if self.emoticons.has_key( user ): self.emoticons[user].update( {shortcut:id} ) else: self.emoticons.update( {user:{shortcut:id}} ) def get( self, user=None ): if user != None: user = user.lower() if self.emoticons.has_key( user ): return self.emoticons[ user ] else: return [] class Conversation( gobject.GObject ): '''This class is an abstraction of a conversation, it is used to separate the data from the GUI in a conversation to let us have single and tabbed windows with the same codebase (also MVC is good :P)''' def __init__( self, controller, switchboard ): '''Contructor''' gobject.GObject.__init__(self) self.callbackIdList = [] self.setSwitchboard( switchboard ) self.controller = controller self.parser = controller.unifiedParser self.config = controller.config self.contactManager = switchboard.msn.contactManager self.lastMessageMail = '' # the mail of the user who sent the last message self.isCurrent = False # if True this is the tab that has the focus self.tabNum = None # the tabnumber of this conversation self.closed = False self.textBuffer = gtk.TextBuffer() self.autoreplySent = False # if true we allready sent the autoreply message self.log = False self.inputText = '' self.theme = controller.theme self.parentConversationWindow = None switchboard.msn.connect( 'user-attr-changed', self.onUserAttrChanged ) switchboard.msn.connect( 'custom-emoticon-transfered', self.onCustomEmoticonTransfered ) #TODO: from now on this signals will be emitted by conversation window or conversationUI controller.connect( 'color-changed', self.onColorChanged ) controller.connect( 'font-changed', self.onFontChanged ) self.ui = ConversationUI.ConversationUI( self.controller, self ) self.log = Log.Log( self.config, self.getMembers() ) self.customEmoticons = CustomEmoticons() self.parser = self.controller.unifiedParser self.sendOffline = False self.lastSpeaker = '' self.user = switchboard.user def getTabNum( self ): return self.tabNum def getUI(self): return self.ui def getSwitchboard(self): return self.switchboard def close( self ): '''close the tab''' self.log.autosave() self.parentConversationWindow.close_tab( self, self.tabNum ) def onColorChanged( self, controller, colorStr ): self.setFontColor(colorStr) self.ui.getInput().applyAttrsToInput() def onFontChanged( self, controller, font, bold, italic, size ): self.setFont(font, italic, bold, size) self.ui.toolbar.setFontBold( bold ) self.ui.toolbar.setFontItalic( italic ) self.ui.getInput().applyAttrsToInput() def onUserAttrChanged(self, msnp, contact ): if contact.email in self.getMembers(): if self.isCurrent: self.parentConversationWindow.set_title(self.getTitle(isEscaped=False)) self.parentConversationWindow.set_icon(self.controller.theme.statusToPixbuf(contact.status)) def setFontColor( self, color ): '''set the color of the user text''' self.config.setUserValue('fontColor', color) def getFontColor( self ): '''return the user color''' return self.config.getUserValue('fontColor') def setFont( self, font, italic=False, bold=False, size=10 ): '''set the font of the user text''' self.setFontFace(font) self.setFontItalic(italic) self.setFontBold(bold) self.setFontSize(size) def getFontFace( self ): '''return the user font''' return self.config.getUserValue('fontFace') def setFontFace( self, value ): '''return the user font''' self.config.setUserValue('fontFace', value) def setFontBold( self, value ): '''set the attribute bold to value''' if value: self.config.setUserValue('fontBold', '1') else: self.config.setUserValue('fontBold', '0') def getFontBold( self ): '''return the bold value''' return self.config.getUserValue('fontBold') == '1' def setFontSize( self, value ): '''set the attribute size to value''' if value: self.config.setUserValue('fontSize', str(value)) else: self.config.setUserValue('fontSize', str(value)) def getFontSize( self ): '''return the size value''' return int(self.config.getUserValue('fontSize')) def setFontItalic( self, value ): '''set the attribute italic to value''' if value: self.config.setUserValue('fontItalic', '1') else: self.config.setUserValue('fontItalic', '0') def getFontItalic( self ): '''return the log value''' return self.config.getUserValue('fontItalic') == '1' def setFontUnderline( self, value ): '''set the attribute underline to value''' if value: self.config.setUserValue('fontUnderline', '1') else: self.config.setUserValue('fontUnderline', '0') def getFontUnderline( self ): '''return the underline value''' return self.config.getUserValue('fontUnderline') == '1' def setFontStrike( self, value ): '''set the attribute underline to value''' if value: self.config.setUserValue('fontStrike', '1') else: self.config.setUserValue('fontStrike', '0') def getFontStrike( self ): '''return the underline value''' return self.config.getUserValue('fontStrike') == '1' def getTextBuffer( self ): '''return the textbuffer''' return self.textBuffer def getTitle( self, isShortened=True, isEscaped=True ): '''return a title according to the users in the conversation''' members = self.getMembers() if len( members ) > 1: title = _( 'Group chat' ) elif len( members ) == 1: title = emesenelib.common.unescape( self.switchboard.getNick(members[0], (self.config.getUserValue('useAliasIfAvailable','1')=='1'))) else: title = '' if isEscaped: title = emesenelib.common.escape(title) return title def getUser( self ): '''return the (local) user mail''' return self.switchboard.user def getRTL( self, message ): '''check whether it's an right-to-left string''' try: if pango.find_base_dir(message, -1) == pango.DIRECTION_RTL: return '1' finally: return '0' def getStyle( self, message='' ): '''return the style string to use in the sendMessage method''' effectValue = '' if self.getFontBold(): effectValue += 'B' if self.getFontItalic(): effectValue += 'I' if self.getFontUnderline(): effectValue += 'U' if self.getFontStrike(): effectValue += 'S' color = self.getFontColor().replace('#', '') color = color[ 4:6 ] + color[ 2:4 ] + color[ :2 ] return "X-MMS-IM-Format: FN=" + self.getFontFace().replace( ' ', '%20' ) + "; EF=" + effectValue + "; CO=" + color + "; PF=0;RL=" + self.getRTL(message) def getId( self ): '''return the id of the switchboard''' return self.switchboard.getId() def getOnlineUsers( self ): '''This method returns a list ol mails of the contacts who are not offline''' return self.switchboard.getOnlineUsers() def invite( self, mail ): '''invite a user to the conversation''' self.switchboard.invite( mail ) def getMembers( self ): '''return a list of the members in the conversation''' members = self.switchboard.getMembers() if len(members) != 0: return members members = self.switchboard.getInvitedMembers() if len(members) != 0: return members return [self.switchboard.firstUser] def getWindow( self ): '''return the window that hold this conversation''' return self.parentConversationWindow def setWindow( self, window ): '''set the window that hold this conversation''' self.parentConversationWindow = window def setIsCurrent(self, current ): '''set the isCurrent attribute, if true, this conversation is the tab that is shown''' self.isCurrent = current def getIsCurrent(self): '''return the value of isCurrent''' return self.isCurrent def receiveNudge( self, switchboard, mail ): '''This method is called when a nudge is received in the switchboard''' self.appendOutputText( None, _( "%s just sent you a nudge!" )%\ self.parser.getParser(self.switchboard.getUserDisplayName( mail )).get(), 'information') self.parentConversationWindow.setUrgency() def receiveOIM( self, nick, message, date ): '''This method is called when a offline message is received''' self.appendOutputText( nick, message, 'offline_incoming', timestamp=time.mktime(date) ) self.parentConversationWindow.setUrgency() def receiveError( self, msnp, to, message, error ): '''This method is called when a error message is received''' self.appendOutputText( 'Error', "Can\'t send message (%s)\n%s" % (error, message), 'error') self.parentConversationWindow.setUrgency() def onReceiveMessage( self, switchboard, mail, nick, message, format, charset ): '''This method is called when a message is received in the switchboard''' self.controller.conversationManager.emit( 'receive-message', self, mail, nick, message, format, charset ) def onInkMessage( self, switchboard, mail, filename ): '''This method is called when an ink message is received in the switchboard''' print "On ink: %s, %s" % (mail, filename) self.appendOutputText( mail, quote(filename), 'ink_incoming') def do_receive_message( self, mail, nick, message, format, charset ): '''This method is called when a message is received in the switchboard''' if not self.parentConversationWindow.has_toplevel_focus() or not self.isCurrent: self.ui.setMessageWaiting(mail) self.parentConversationWindow.setUrgency() self.parentConversationWindow.show() else: self.ui.setDefault(mail) if self.config.getUserValue( 'autoReply' ) == '1' and not self.autoreplySent: self.switchboard.sendMessage( 'AutoMessage: ' + self.config.getUserValue( 'autoReplyMessage' )) self.appendOutputText( None, 'AutoMessage: %s\n'%self.config.getUserValue( 'autoReplyMessage' ), 'information') self.autoreplySent = True self.appendOutputText( mail, message, 'incoming', self.parseFormat(mail, format)) def userJoin( self, switchboard, mail ): '''This method is called when someone joing the conversation''' if self.isCurrent: self.parentConversationWindow.set_title( self.getTitle(False,False) ) self.ui.update() def userLeave( self, switchboard, mail ): '''method called when someone leaves the conversation''' self.appendOutputText( "", _( "%s has left the conversation" )%\ self.parser.getParser(self.switchboard.getUserDisplayName( mail )).get(), 'information') self.ui.update() if self.isCurrent: self.parentConversationWindow.set_title( self.getTitle(False,False) ) def userOffline( self, switchboard, mail ): '''method called when someone goes offline in the conversation''' self.appendOutputText( None, _( "%s is now offline" )%\ self.switchboard.getUserDisplayName(self.switchboard.getUserDisplayName( mail ).parse()), 'information') self.ui.update() def userOnline( self, switchboard, mail ): '''method called when someone goes online in the conversation''' self.appendOutputText( None, _( "%s is now online" )%\ self.switchboard.getUserDisplayName(self.switchboard.getUserDisplayName( mail ).parse()), 'information') self.ui.update() def sbStatusChange( self, switchboard ): self.ui.update() def inviteUser( self, mail ): '''method called when the user selects a friend in the invite dialog''' self.invite( mail ) self.ui.messageWaiting[mail] = False self.ui.contactTyping[mail] = False self.ui.update() def doNudge( self ): '''this method is called when the user clicks the nudge button''' self.switchboard.sendNudge() self.appendOutputText( None, _( "you have sent a nudge!" ), 'information') def reconnect( self ): '''reconnect the switchboard''' users = self.getMembers() msn = self.switchboard.msn self.setSwitchboard( msn.newSwitchboard() ) for i in users: self.switchboard.invite( i ) self.autoreplySent = False def splitMessage( self, message ): '''Split large messages''' messageChunks = [] messageLen = len(message) msgStart = 0 while msgStart < messageLen: chunk = message[msgStart:msgStart+MAX_MESSAGE_LENGTH] chunkLen = len(chunk) if chunkLen == MAX_MESSAGE_LENGTH: chunkEnd = chunk.rfind(' ') if chunkEnd!=-1 and chunkEnd>0: messageChunks.append( chunk[0:chunkEnd] ) msgStart += chunkEnd+1 else: msgStart += chunkLen messageChunks.append( chunk ) else: msgStart += chunkLen messageChunks.append( chunk ) return messageChunks def do_send_message( self, message ): '''Send the message from the UI input. This chooses between OIM and switchboard to send the message.''' remoteMail = self.switchboard.firstUser remoteStatus = self.controller.getContactStatus( remoteMail ) sbReady = self.switchboard.status != "established" if sbReady and remoteStatus == 'FLN': dialog = Dialogs.Confirm( self.parentConversationWindow, \ _( "Are you sure you want to send a offline message to %s" ) % remoteMail, gtk.STOCK_YES ) if self.sendOffline == True or dialog.run() == gtk.RESPONSE_ACCEPT: self.sendOffline = True self.switchboard.msn.msnOIM.send( remoteMail, message ) self.appendOutputText( self.user, message, 'outgoing') return else: text = _('not sent "%s" to "%s"') % (message, remoteMail) self.appendOutputText( _('Offline'), text, 'outgoing') return if self.switchboard.status == 'closed': self.reconnect() messageChunks = self.splitMessage( message ) for chunk in messageChunks: try: self.switchboard.sendCustomEmoticons( chunk ) self.switchboard.sendMessage( chunk, self.getStyle( chunk ) ) self.appendOutputText(self.user, chunk, 'outgoing') except Exception, e: print str( e ) self.reconnect() self.do_send_message( ''.join( messageChunks[messageChunks.index( chunk ):] ) ) return def sendMessage( self, message ): '''send a message to the conversation''' self.controller.conversationManager.emit( 'send-message', self, message ) def sendIsTyping( self ): '''an easy method to send the is typing message''' if self.switchboard.status == 'closed': self.reconnect() try: self.switchboard.sendIsTyping() except Exception: self.reconnect() self.sendIsTyping() def parseFormat( self, mail, format ): '''parse the format of a mail and return the style''' # if the useFriendsUnifiedFormat flag is set, then return that format if bool(int(self.config.getUserValue( 'useFriendsUnifiedFormat', '0' ))): font = (self.config.getUserValue( 'friendsUnifiedFont', 'Sans' )) color = (self.config.getUserValue( 'friendsUnifiedColor', '#000000' )) return 'font-family: ' + font + ';color: ' + color + ';' # FN=Sans; EF=; CO=000000; PF=0 style = '' if format.find( "FN=" ) != -1: font = format.split( 'FN=' )[ 1 ].split( ';' )[ 0 ].replace( '%20', ' ' ) style += 'font-family: ' + font + ';' if format.find( "CO=" ) != -1: color = format.split( 'CO=' )[ 1 ].split( ';' )[ 0 ] if len( color ) == 3: color = color[ 2 ] + color[ 1 ] + color[ 0 ] style += 'color: #' + color + ';' else: color = color.zfill(6) if len( color ) == 6: color = color[ 4:6 ] + color[ 2:4 ] + color[ :2 ] style += 'color: #' + color + ';' if format.find( "EF=" ) != -1: effect = set(format.split( 'EF=' )[ 1 ].split( ';' )[ 0 ]) if "B" in effect: style += 'font-weight: bold;' if "I" in effect: style += 'font-style: italic;' if "U" in effect: style += 'text-decoration: underline;' if "S" in effect: style += 'text-decoration: line-through;' return style def setSwitchboard( self, switchboard ): '''set a new sitchboard for the conversation. usefull if the conversation is closed and the other user start a new one''' signalDict = { 'nudge': self.receiveNudge, 'message': self.onReceiveMessage, 'ink-message': self.onInkMessage, 'user-join': self.userJoin, 'user-leave': self.userLeave, 'typing': self.receiveTyping, 'custom-emoticon-received': self.onCustomEmoticonReceived, } while len( self.callbackIdList ) > 0: self.switchboard.disconnect( self.callbackIdList.pop() ) self.switchboard = switchboard for signalName in signalDict.keys(): self.callbackIdList.append( self.switchboard.connect( signalName, signalDict[signalName]) ) self.autoreplySent = False def onCustomEmoticonReceived(self, switchboard, shortcut, msnobj): '''call when a smiley is received''' self.customEmoticons.setNew(msnobj.creator, shortcut, msnobj.sha1d) def onCustomEmoticonTransfered( self, switchboard, to, msnobj, path ): '''call when a smiley is transfered''' #print "*" * 100 self.ui.textview.setCustomObject( msnobj.sha1d, path, type='application/x-emesene-emoticon' ) def appendOutputText(self, username, text, type, style = None, timestamp = None): '''append the given text to the outputBuffer''' if type.startswith('ink_'): type = type[4:] ink = True else: ink = False if type != 'incoming' and type != 'outgoing': self.lastSpeaker = '' elif username == self.lastSpeaker: type = 'consecutive_' + type self.lastSpeaker = username usedstyle = None if style: usedstyle = style if username == self.switchboard.user: nick = emesenelib.common.escape(self.switchboard.msn.nick) elif username != None: nick = emesenelib.common.escape(self.switchboard.getUserDisplayName(username)) else: nick = '' if timestamp == None: timestamp = time.time() displayedText = self.controller.getConversationLayoutManager().layout(username, text, usedstyle, self, type, timestamp, ink) self.log.add(type, nick, text, timestamp, displayedText) try: self.ui.textview.display_html( displayedText.encode() ) except Exception, e: print 'error trying to display "' + displayedText + '"' print e self.parentConversationWindow.scrollToBottom( self.tabNum ) self.textBuffer.place_cursor( self.textBuffer.get_end_iter() ) self.parentConversationWindow.scrollToBottom( self.tabNum ) def getStatus( self ): return self.switchboard.status def setStatus( self, value ): self.switchboard.setStatus( value ) def isClosed( self ): return self.closed def setClosed( self, value ): self.closed = value if value == True: self.log.autosave() self.switchboard.leaveChat() def receiveTyping( self, switchboard, mail ): '''This method is called when a is typing message is received in the switchboard''' self.ui.setTyping(mail) def getMembersDict( self ): '''return a dict with email as key and contact instance as value''' userDict = {} for i in self.getMembers(): if self.contactManager.getContact( i ) != None: userDict[ i ] = self.contactManager.getContact( i ) else: userDict[ i ] = self.contactManager.getDummyContact( i ) return userDict def getTextTag( self ): '''return a text tag from the current style''' tag = gtk.TextTag() tag.set_property( 'font', self.getFontFace() ) tag.set_property( 'size-points', self.getFontSize() ) tag.set_property( "foreground" , self.getFontColor() ) if self.getFontBold(): tag.set_property( "weight" , pango.WEIGHT_BOLD ) if self.getFontItalic(): tag.set_property( "style" , pango.STYLE_ITALIC ) if self.getFontUnderline(): tag.set_property( "underline-set" , True ) tag.set_property( "underline" , pango.UNDERLINE_SINGLE ) else: tag.set_property( "underline-set" , True ) tag.set_property( "underline" , pango.UNDERLINE_NONE ) tag.set_property( "strikethrough" , self.getFontStrike() ) return tag gobject.type_register(Conversation)