/*************************************************************************** chatmaster.cpp - description ------------------- begin : Sat Jan 18 2003 copyright : (C) 2003 by Mike K. Bennett email : mkb137b@hotmail.com ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * ***************************************************************************/ #include "chatmaster.h" #include "../contact/contact.h" #include "../contact/contactextension.h" #include "../contact/msnobject.h" #include "../model/contactlist.h" #include "../network/applications/applicationlist.h" #include "../network/applications/mimeapplication.h" #include "../network/applications/p2papplication.h" #include "../network/applications/filetransfer.h" #include "../network/applications/filetransferp2p.h" #include "../network/applications/inktransferp2p.h" #include "../network/applications/msnobjecttransferp2p.h" #include "../network/chatmessage.h" #include "../network/msnswitchboardconnection.h" #include "../currentaccount.h" #include "../kmessapplication.h" #include "../kmessdebug.h" #include "../utils/kmessconfig.h" #include "chat.h" #include "chatwindow.h" #include <QFile> #include <QImageReader> #include <QTextDocument> #include <QTest> #include <KWindowSystem> #ifdef Q_WS_X11 # include <QX11Info> #endif // Settings for debugging #define CHATMASTER_SEND_FILES_MSNFTP 0 // The constructor ChatMaster::ChatMaster(QObject *parent) : QObject(parent), initialized_(false) { setObjectName("ChatMaster"); } // The destructor ChatMaster::~ChatMaster() { disconnected(); // Delete all open windows kmDebug() << chatWindows_; qDeleteAll( chatWindows_ ); chatWindows_.clear(); // Cleanup pending messages qDeleteAll( pendingMimeMessages_ ); pendingMimeMessages_.clear(); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "DESTROYED."; #endif } // The connection has been established void ChatMaster::connected() { // There are no chats to reactivate if( chats_.isEmpty() ) { return; } #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Trying reactivation of" << chats_.count() << "open chats."; #endif QList<ChatWindow*> windowsList; // Go through all the chats and reactivate them foreach( Chat *chat, chats_ ) { // Reactivate right away chats which have a connection already. Should never happen though. if( chat->getSwitchboardConnection() != 0 ) { chat->setEnabled( true ); // Mark this chat's window for reactivation if( ! windowsList.contains( chat->getChatWindow() ) ) { windowsList.append( chat->getChatWindow() ); } continue; } const QStringList participants( chat->getParticipants() ); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Reactivating chat:" << participants; #endif // Check if the participants list is empty: if so, there's something fishy going on. if( participants.isEmpty() ) { kmWarning() << "Tried to reactivate chat with no contacts - has a window?" << ( chat->getChatWindow() != 0 ); chat->getChatWindow()->removeChatTab( chat ); continue; } // Do not re-enable group chats, as they need a new invitation to work again if( participants.count() > 1 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "This is a group chat, skipping it."; #endif continue; } const QString &handle = participants.first(); // Only reactivate the chat if this contact is in the connected account's list if( currentAccount_->getContactByHandle( handle ) == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "This chat is with a non-listed contact, closing it."; #endif chat->getChatWindow()->removeChatTab( chat ); continue; } #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Reactivating chat with listed contact:" << handle; #endif // Create a new switchboard, setting it as empty but with an hint about // who to call if the user wants to chat again. MsnSwitchboardConnection *switchboard = createSwitchboardConnection( 0, handle ); if( switchboard ) { chat->setSwitchboardConnection( switchboard ); chat->setEnabled( true ); // Mark this chat's window for reactivation if( ! windowsList.contains( chat->getChatWindow() ) ) { windowsList.append( chat->getChatWindow() ); } } } // Go through all the windows and reactivate them foreach( ChatWindow *window, windowsList ) { window->setUIState( ChatWindow::Connected ); } } // The user has disconnected, so close all open switchboard connections and disable the chats void ChatMaster::disconnected() { // Don't waste time disabling the chats when exiting. KMessApplication *kmessApp = static_cast<KMessApplication*>( kapp ); if( kmessApp->quitSelected() ) { return; } if( ! chatWindows_.isEmpty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Disabling" << chatWindows_.count() << "open chat windows."; #endif // Go through all the chats foreach( ChatWindow *window, chatWindows_ ) { // Disable the chats so they can't receive user input anymore window->setUIState( ChatWindow::Disconnected ); } } // close down all switchboards first. if( ! switchboardConnections_.isEmpty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Closing" << switchboardConnections_.count() << "open switchboards."; #endif // give each switchboard a chance to close properly. foreach( MsnSwitchboardConnection *conn, switchboardConnections_ ) { conn->closeConnection(); } } if( ! chats_.isEmpty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Closing" << chats_.count() << "open chats."; #endif // Go through all the chats and disable them foreach( Chat *chat, chats_ ) { chat->setEnabled( false ); } } // Cleanup switchboard connections. qDeleteAll( switchboardConnections_ ); switchboardConnections_.clear(); } // KMess is establishing a connection. void ChatMaster::connecting() { // There are no chats modify. if( chats_.isEmpty() ) { return; } #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Disabling reconnect button of" << chats_.count() << "open chats."; #endif QList<ChatWindow*> windowsList; // Go through all the chats and add their window. foreach( Chat *chat, chats_ ) { if( ! windowsList.contains( chat->getChatWindow() ) ) { windowsList.append( chat->getChatWindow() ); } } // Go through all the windows and disable their reconnect button. foreach( ChatWindow *window, windowsList ) { window->setUIState( ChatWindow::Connecting ); } } // Forward a new switchboard server request signal coming from an existing switchboard connection void ChatMaster::forwardRequestNewSwitchboard( QString handle ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Forwarding the new switchboard request signal for " << handle << "." << endl; #endif emit requestSwitchboard( handle, ChatInformation::CONNECTION_REFRESH ); } // Forward a request to add or remove a contact from the contactlist, which comes from the ContactFrame. void ChatMaster::forwardContactAdded( QString handle, bool isAdded ) { if( isAdded ) { emit addContact( handle ); } else { emit removeContact( handle, false ); } } // Forward a request to block or unblock a contact, which comes from the ContactFrame. void ChatMaster::forwardContactBlocked( QString handle, bool isBlocked ) { if( isBlocked ) { emit blockContact( handle ); } else { emit unblockContact( handle ); } } // Return the application list for a given contact ApplicationList * ChatMaster::getApplicationList(const QString &handle) { ApplicationList *appList = 0; // Get the contact. ContactBase *contact = currentAccount_->getContactByHandle(handle); if(KMESS_NULL(contact)) return 0; if( handle == currentAccount_->getHandle() ) { kmWarning() << "Cannot start applications with yourself."; return 0; } // Get the application list if( contact->hasApplicationList() ) { appList = contact->getApplicationList(); } else { // Create object if it wasn't created yet for this contact. #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Creating application list for contact " << handle; #endif appList = contact->createApplicationList(); // Connect signals from the applist connect( appList, SIGNAL( newApplication(Application*) ), // A new application was created. this, SLOT( slotConnectApplication(Application*) )); connect( appList, SIGNAL( putMsg(const MimeMessage&, const QString&, bool) ), // Request to send message this, SLOT( slotDeliverMimeMessage(const MimeMessage&, const QString&, bool) )); } return appList; } // Return the chat tab which uses the given switchboard connection Chat *ChatMaster::getChatBySwitchboard( const MsnSwitchboardConnection *connection ) { // Look through the chat windows for an exclusive chat with the given contact foreach( Chat *chat, chats_ ) { if( chat->getSwitchboardConnection() == connection ) { return chat; } } return 0; } // Return the chat window which uses the given switchboard connection ChatWindow * ChatMaster::getChatWindowBySwitchboard(const MsnSwitchboardConnection *connection) { Chat *chat = getChatBySwitchboard( connection ); if( chat != 0 ) { return chat->getChatWindow(); } return 0; } // Return the chat where we're having an conversation with the given contact. Chat *ChatMaster::getContactsChat( const QString &handle, bool privateChat ) { Chat *result = 0; // Look through the chats for an exclusive one with the contact given foreach( Chat *chat, chats_ ) { if( chat->isContactInChat( handle, true ) ) { return chat; // best candidate found } else if( ! privateChat && chat->isContactInChat( handle ) ) { result = chat; // keep it as a second choice } } return result; } // Return the chat where we're having a conversation with the given contacts. // The privateChat parameter is ignored. Chat *ChatMaster::getContactsChat( const QStringList &handles, bool privateChat ) { // Do nothing if there are no open chats or if the list is empty if( handles.isEmpty() || chats_.isEmpty() ) { return 0; } // If the list only has one contact, just call the single handle version of this method. if( handles.count() == 1 ) { return getContactsChat( handles.first(), privateChat ); } Chat *result = 0; // Look through the chats for a group chat with all the contacts of the list foreach( Chat *chat, chats_ ) { bool found = true; // Check every contact in the list to see if they're all present in this chat for( QStringList::ConstIterator it = handles.begin(); it != handles.end(); ++it ) { if( ! chat->isContactInChat( *it ) ) { found = false; break; } } if( found ) { result = chat; break; } } return result; } // Return the chat connection where we're having an conversation with the given contact. MsnSwitchboardConnection *ChatMaster::getContactSwitchboardConnection( const QString &handle, bool doRequirePrivateChat ) { foreach( MsnSwitchboardConnection *connection, switchboardConnections_ ) { if( ! connection->isContactInChat( handle ) ) { continue; } // If the wanted contact is in this chat, it's 99% sure this is the one we want, except when we want a private chat, but this is not if( ! connection->isExclusiveChatWithContact( handle ) && doRequirePrivateChat ) { continue; } return connection; } return 0; } // Initialize the class bool ChatMaster::initialize() { if ( initialized_ ) { kmDebug() << "Already initialized."; return false; } // Save the current account reference currentAccount_ = CurrentAccount::instance(); // Get the tabbed chat settings chatTabbedMode_ = currentAccount_->getTabbedChatMode(); // Connect the contact list const ContactList *contactList = currentAccount_->getContactList(); connect( contactList, SIGNAL( contactChangedMsnObject(Contact*) ), this, SLOT ( slotContactChangedMsnObject(Contact*) ) ); // Update chat grouping when settings are changed connect( currentAccount_, SIGNAL( changedChatStyleSettings() ), this, SLOT ( updateChatGrouping() ) ); // If the user has added him/herself as a contact, we need to update its picture // when a new display picture is set connect( currentAccount_, SIGNAL( changedMsnObject() ), this, SLOT ( slotContactChangedMsnObject() ) ); initialized_ = true; return true; } // Check whether the contact is in any of the existing chat windows. bool ChatMaster::isContactInChat( const QString &handle ) { // Find the chat where we have an exclusive chat with the given contact return ( getContactsChat( handle, false ) != 0 ); } // Verify if a new chat can be opened and requests a switchboard void ChatMaster::requestChat( QString handle ) { ChatInformation::ConnectionType connectionType; // Forbid chatting with yourself if( handle == currentAccount_->getHandle() ) { kmWarning() << "Cannot chat with yourself."; return; } // Look through the chats for an exclusive chat with the given contact Chat *chat = getContactsChat( handle, true ); // No need to open another chat, notify about the presence of the existing one. if( chat != 0 ) { raiseChat( chat, true ); return; } // Get the contact, see if it's online CurrentAccount *currentAccount = currentAccount_; ContactBase *contact = currentAccount->getContactByHandle( handle ); // TODO we shouldn't decide if the chat will be in offline mode, // we should see if the CAL return value from SB is 217 if( contact && contact->getStatus() == STATUS_OFFLINE ) { // Either the contact is offline or invisible. // We'll start an offline chat. connectionType = ChatInformation::CONNECTION_OFFLINE; #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Starting an offline chat with " << handle; #endif } else { // Start a normal chat with an online contact connectionType = ChatInformation::CONNECTION_CHAT; #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Requesting a switchboard to chat with " << handle; #endif } MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true ); if( switchboard != 0 ) { // The user request chat with user that have already opened one SB // So, re-use the SB and force the raise of chat! createChat( switchboard, true ); return; } // Append the requested chat with handle to the list // So when the chat will be created it will be raised requestedChats_.append( handle ); emit requestSwitchboard( handle, connectionType ); } // A chat is closing void ChatMaster::slotChatClosing( Chat *chat ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Marking chat as closing."; #endif // Make sure this chat window don't receive new messages. closingChats_.append( chat ); // The contains check avoids deleting references to old switchboards which may be outdated and don't exist anymore. MsnSwitchboardConnection *switchboard = chat->getSwitchboardConnection(); if( switchboard && switchboardConnections_.contains( switchboard ) ) { switchboardConnections_.removeAll( switchboard ); delete switchboard; } } // A chat was destroyed void ChatMaster::slotChatDestroyed( QObject *chatObject ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Removing chat from list."; #endif // Remove from the list Chat *chat = static_cast<Chat*>( chatObject ); chats_ .removeAll( chat ); closingChats_.removeAll( chat ); } // A chat window was destroyed void ChatMaster::slotChatWindowDestroyed(QObject *chatWindow) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Removing chat window from list."; #endif // Remove from the list ChatWindow *window = static_cast<ChatWindow*>( chatWindow ); chatWindows_.removeAll( window ); } // A new application was created for a contact. void ChatMaster::slotConnectApplication(Application *application) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Connecting application signals."; #endif connect( application, SIGNAL( applicationMessage(const ChatMessage&) ), this, SLOT( showSpecialMessage(const ChatMessage&) ) ); connect( application, SIGNAL( updateApplicationMessage(const QString&,const QString&) ), this, SIGNAL( updateApplicationMessage(const QString&,const QString&) ) ); // Special connections for applications which were started dynamically to handle an incoming data stream. if( application->getMode() == Application::APP_MODE_DATACAST ) { if( qobject_cast<InkTransferP2P*>( application ) != 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Connecting additional signals for InkTransferP2P."; #endif connect( application, SIGNAL( inkReceived(const QString&,const QString&,InkFormat) ), this, SLOT( slotGotInkMessage(const QString&,const QString&,InkFormat) )); } else { kmWarning() << "Unknown datacast application: " << application->metaObject()->className(); } } } // Append the message to the queue, waiting to be delivered void ChatMaster::queueMessage( const MimeMessage &message, const QString &handle, bool privateChatRequired ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Adding message for '" << handle << "' to the queue " << "(size=" << ( pendingMimeMessages_.count() + 1 ) << ")." << endl; #endif // NOTE: Store these pending in ApplicationList class instead? PendingMimeMessage *pending = new PendingMimeMessage; pending->handle = handle; pending->message = message; pending->privateChatRequired = privateChatRequired; // No active connection found. // Request a new chat with the contact, keep the message pending. pendingMimeMessages_.append( pending ); } // Raise an existing chat window. Forcing a raise is reasonable only when the user requests to, since it makes the window // to grab keyboard focus and be raised on top of other windows. void ChatMaster::raiseChat( Chat *chat, bool force ) { // Check if the pointer is valid if( chat == 0 || ! chats_.contains( chat ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Invalid raising request received; chat is probably already closed."; #endif return; } ChatWindow *chatWindow = chat->getChatWindow(); if( chatWindow == 0 || ! chatWindows_.contains( chatWindow ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Invalid raising request received; chat is probably already closed."; #endif return; } if( chatWindow->isVisible() && ! force ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Window has focus already."; #endif return; } // Move the window on the current desktop if it wasn't shown before or if we're forcing its raise // TODO: make this optional, maybe the user doesn't want the windows to span over desktops if( ! chatWindow->isVisible() || force ) { KWindowSystem::setOnDesktop( chatWindow->winId(), KWindowSystem::currentDesktop() ); } // Making windows raise and get focus interrupts the user... it's better to make them appear minimized, // and let them just flash in the taskbar. if( ! force ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Showing minimized window."; #endif // Required for new chats chatWindow->showMinimized(); return; } // Activate the selected tab chatWindow->setCurrentChat( chat ); // Notify about the new window and force it to show and receive focus. Only use upon user request. #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Forcing window raise."; #endif // Show the chat's window if( chatWindow->isMinimized() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Window was minimized, showing it."; #endif chatWindow->showNormal(); } else { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Window was not minimized, showing it again."; #endif chatWindow->show(); } #ifdef Q_WS_X11 // Bypass focus stealing prevention (code from Quassel) QX11Info::setAppUserTime( QX11Info::appTime() ); #endif // Workaround a bug in KWin: force the window to get focus if( chatWindow->windowState() & Qt::WindowMinimized) { chatWindow->setWindowState( (chatWindow->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive ); chatWindow->show(); } // Set the keyboard focus to the chat window chatWindow->raise(); KWindowSystem::activateWindow( chatWindow->winId() ); KApplication::setActiveWindow( chatWindow ); } /** * @brief Send all pending mime messages for the contact. * @param handle The contact to send messages for. * @param connection The switchboard connection that can be used to send the messages. */ 00789 void ChatMaster::sendPendingMimeMessages( const QString &handle, MsnSwitchboardConnection *connection ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Sending pending messages for contact " << handle << ", if any."; #endif QList<PendingMimeMessage*> sentMessages; bool isPrivateChat = (connection->getContactsInChat().size() < 2); foreach( PendingMimeMessage *pendingMimeMessage, pendingMimeMessages_ ) { // Ignore messages for other contacts if( pendingMimeMessage->handle != handle ) { // Get next message continue; } // Ignore messages that can only be sent on a private chat. if( pendingMimeMessage->privateChatRequired && ! isPrivateChat ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Switchboard is not a private chat, keeping message in queue."; #endif // Get next message continue; } // Check when the connection is busy again if( connection->isBusy() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Switchboard is busy again, keeping remaining messages in the queue."; #endif goto cleanup_return; } // Check whether the contact is still in the chat. if( ! connection->isConnected() || ! connection->getContactsInChat().contains( handle ) ) { // getContactsInChat() may be empty, don't re-invite the last contact to deviver the message #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Switchboard is not connected or contact left chat, keeping remaining messages in the queue."; #endif goto cleanup_return; } // Send the message connection->sendApplicationMessage( pendingMimeMessage->message ); sentMessages.append( pendingMimeMessage ); } cleanup_return: // Do deletion out of the loop, just to be sure. foreach( PendingMimeMessage *sentMessage, sentMessages ) { pendingMimeMessages_.removeAll( sentMessage ); delete sentMessage; } } // Deliver a chat message to the chat window (can be an Application message or an Offline-IM from MsnNotificationConnection). void ChatMaster::showSpecialMessage( const ChatMessage &message, const MsnSwitchboardConnection *switchboardChat ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Delivering application/ink/OIM chat message to chat window."; #endif const QString &handle = message.getContactHandle(); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Message info, Contact: " << handle << " - Message body: '" << message.getBody() << "' - " "Message type: " << message.getType() << " - Message class: " << message.getContentsClass() << "." << endl; #endif Chat *chat; if( switchboardChat != 0 ) { chat = getChatBySwitchboard( switchboardChat ); } else { chat = getContactsChat( handle, true ); } // Deliver the message to the contact's chat window if( chat != 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Delivering message."; #endif // The chat window is currently closing if( closingChats_.contains( chat ) ) { if( message.isNormalMessage() ) { // TODO: Sometimes, you close a window when the other contact sends a new message, and you lose it. // Maybe it's possible to avoid the loss, for example with a notification balloon to notify of the message. kmWarning() << "Chat window is closing, suppressing message '" << message.getBody() << "' from '" << handle << "'."; } #ifdef KMESSDEBUG_CHATMASTER else { kmDebug() << "Chat window is closing, suppressing message."; } #endif return; } chat->receivedMessage( message ); return; } // No window is available - we should check if it's needed to create a new window, or if the message // can be safely ignored. switch( message.getContentsClass() ) { case ChatMessage::CONTENT_MESSAGE: case ChatMessage::CONTENT_MESSAGE_INK: case ChatMessage::CONTENT_NOTIFICATION_NUDGE: case ChatMessage::CONTENT_NOTIFICATION_WINK: case ChatMessage::CONTENT_APP_INVITE: #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Message is important, delivering it."; #endif break; default: #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Message is not important, will be ignored."; #endif return; } // Check if a switchboard is present MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true ); if( switchboard == 0 ) { // Create a new offline switchboard connection to deliver the message. It can be converted to an // online switchboard one if needed. switchboard = startSwitchboard( ChatInformation( 0, handle, "", 0, "", "", ChatInformation::CONNECTION_OFFLINE ) ); if( switchboard == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Could not create an offline switchboard."; #endif return; } } // Create a new chat window for the message chat = createChat( switchboard ); if( chat == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Could not create a chat window."; #endif return; } // Deliver the message to the new chat chat->receivedMessage( message ); } // Display an MSN Object that was received. void ChatMaster::showMsnObject( const QString &handle, const MsnObject &msnObject, Chat *chat ) { QString fileName; QString emoticonCode; // Get the contact // Note I don't use msnObject.getCreator, because I don't want to trust the other client! ContactBase *contact = 0; Contact *msnContact = 0; // Get msn object filename fileName = KMessConfig::instance()->getMsnObjectFileName( msnObject ); // Handle the picture switch( msnObject.getType() ) { case MsnObject::DISPLAYPIC: // Get the real MSN contact from the MSN contact list. // Picture is stored in the ContactExtension because it's not an official property of the contact (not stored server-side). msnContact = currentAccount_->getContactList()->getContactByHandle(handle); if(KMESS_NULL(msnContact)) return; msnContact->getExtension()->setContactPicturePath( fileName ); pendingDisplayPictures_.removeAll( handle ); break; case MsnObject::WINK: // Show the Wink in the chat window where the original datacast message came from. if(KMESS_NULL(chat)) return; chat->showWink( handle, fileName, msnObject.getFriendly() ); break; case MsnObject::EMOTICON: // Inform the contact that a custom emoticon can be parsed. contact = currentAccount_->getContactByHandle(handle); if(KMESS_NULL(contact)) return; emoticonCode = contact->getEmoticonCode(msnObject.getDataHash()); // always put before addEmoticonFile()! contact->addEmoticonFile( msnObject.getDataHash(), fileName ); // Update all chats where the contact is present foreach( Chat *chatUpdate, chats_ ) { if( chatUpdate->isContactInChat( handle ) ) { chatUpdate->updateCustomEmoticon( handle, emoticonCode ); } } break; case MsnObject::BACKGROUND: // handle background transfers default: kmWarning() << "Unable to handle MsnObject pictures of type '" << msnObject.getType() << "'."; } } // Determine what to do when a contact changed it's picture. void ChatMaster::slotContactChangedMsnObject( Contact *contact ) { // Avoid trying to download our own display picture, if the user has added him/herself; // instead simply use the current one. // This method can be called to update the user's own picture in list from two places: // 1)on login when the contact is first received, 2)when the user changes the display pic. if( contact == 0 ) { contact = currentAccount_->getContactList()->getContactByHandle( currentAccount_->getHandle() ); // When the connection starts, this method gets called // but the contact list is not ready yet. Cancel. if( contact == 0 ) { return; } } if( contact->getHandle() == currentAccount_->getHandle() ) { contact->getExtension()->setContactPicturePath( currentAccount_->getPicturePath() ); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Updated user's own display picture"; #endif return; } QString handle( contact->getHandle() ); // If the contact no longer likes to display an msn object, remove it if( contact->getMsnObject() == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact '" << handle << "' removed its MSN object"; #endif contact->getExtension()->setContactPicturePath( QString::null ); return; } MsnObject msnObject( *contact->getMsnObject() ); // See if the file already exists. QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( msnObject ) ); if( QFile::exists( objectFileName ) ) { bool imageInvalid = QImageReader::imageFormat( objectFileName ).isEmpty(); if( ! imageInvalid && msnObject.verifyFile( objectFileName ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Picture for '" << handle << "' is already downloaded."; #endif contact->getExtension()->setContactPicturePath( objectFileName ); return; } else { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Picture for '" << handle << "' is already downloaded, but corrupt " << ( imageInvalid ? "(detected by QImageReader)" : "(detected by MsnObject)" ) << ", deleting it: '" << objectFileName << "'" << endl; #endif QFile::remove( objectFileName ); // If the file was set as picture. reset right away. if( contact->getExtension()->getContactPicturePath() == objectFileName ) { #ifdef KMESSDEBUG_CHATMASTER kmWarning() << "Corrupt picture was already set, " << "resetting contact picture." << endl; #endif contact->getExtension()->setContactPicturePath( QString::null ); } } } // If the contact is active in a chat, // download the new picture straight away. MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true ); if( switchboard != 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact '" << handle << "' changed its MSN object."; #endif // Automatically decides which switchboard is the best to use, // this may change during the transfer. startMsnObjectDownload( handle, &msnObject, 0 ); return; } // Queue a request to download it later in the background. // That method will check for the prerequisites to download it, // since that may change between this point and the timedUpdate() call. #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact '" << handle << "' changed its MSN object, queueing a request for it."; #endif // Request the contact's msnobject if( ! pendingDisplayPictures_.contains( handle ) ) { pendingDisplayPictures_.append( handle ); } } // A contact joined to one of our switchboard sessions. void ChatMaster::slotContactJoinedChat( ContactBase *contact ) { // Get switchboard connection from slot sender MsnSwitchboardConnection *connection = static_cast<MsnSwitchboardConnection*>( const_cast<QObject*>( sender() ) ); if(KMESS_NULL(connection)) return; QString handle( contact->getHandle() ); // Send any pending messages, if any if( pendingMimeMessages_.count() > 0 ) { // Find the chat window where the contact is the only participant. if( connection->getContactsInChat().size() > 1 ) { // Contact joined chat conversation with multiple partipipants, ignore this. #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact '" << handle << "' joined multi-chat, not sending pending messages."; #endif } else { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact '" << handle << "' joined chat, checking for pending messages."; #endif // Send the messages if( pendingMimeMessages_.count() > 0 ) { sendPendingMimeMessages( handle, connection ); } } } // Check whether the contact picture is outdated. // Use getContactByHandle() from the MSN contact list to get the msn object. const Contact *fullContact = currentAccount_->getContactList()->getContactByHandle( handle ); if( fullContact != 0 && fullContact->hasP2PSupport() ) { if( fullContact->getMsnObject() != 0 ) { startMsnObjectDownload( fullContact->getHandle(), fullContact->getMsnObject(), 0 ); } } } /** * @brief Deliver an application command to the correct application. */ 01185 void ChatMaster::slotDeliverAppCommand(QString cookie, QString handle, QString command) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Delivering application command."; #endif Application *app = 0; // Get the contact. ContactBase *contact = currentAccount_->getContactByHandle(handle); if(KMESS_NULL(contact)) return; // Get the application list if( contact->hasApplicationList() ) { app = contact->getApplicationList()->getApplicationByCookie(cookie); } // App not found. if( app == 0 ) { kmWarning() << "Application not found to deliver user command (contact=" << contact << ", command=" << command << ")."; return; } // Deliver the 'accept' or 'cancel' command. app->gotCommand( command ); } /** * @brief Deliver a message from an Application to the switchboard connection. * @param message The Mime message to deliver. The message can contain a normal MIME or binary P2P payload. * @param handle The contact the message should be delivered to. * @param privateChatRequired Whether a private chat is required to deliver the message. If not, a multi-chat will be used when available. * * This method automatically determines which switchboard is should use to deliver the message. * When the contact is not present in any of the existing switchboard connections, a new connection request will be made. * The message will be queued for delivery to the switchboard so it can be delivered once it's available. * Conversations with multiple contacts are avoided because of the networking overhead; * since the switchboard is a broadcast channel, each participant receives the message. * * Unlike the direct connection link, the switchboard is only capable of transferring mime messages. * To transfer the P2P data, the P2PApplication has to wrap the message as MimeMessage payload. */ 01231 void ChatMaster::slotDeliverMimeMessage(const MimeMessage &message, const QString &handle, bool privateChatRequired) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Delivering application message for '" << handle << "' to switchboard."; #endif #ifdef KMESSTEST QString contentType( message.getValue("Content-Type") ); KMESS_ASSERT( contentType == "application/x-msnmsgrp2p" || contentType == "text/x-msmsgsinvite" || contentType.section(';', 0, 0) == "text/x-msmsgsinvite" ); // for ; charset= suffix if( contentType == "application/x-msnmsgrp2p" ) { KMESS_ASSERT( message.getValue("P2P-Dest") == handle ); } #endif MsnSwitchboardConnection *connection = getContactSwitchboardConnection(handle, privateChatRequired); if( connection != 0 ) { if( connection->isBusy() ) { // Connection has unacked messages, keep waiting until the switchboard is ready to send #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Switchboard is currently busy, queueing message until the switchboard is ready to send."; #endif queueMessage( message, handle, privateChatRequired ); // Pause application so it won't continue to send much more messages (see P2PApplication::slotSendData()). ApplicationList *appList = getApplicationList(handle); if( appList != 0 ) { appList->pauseApplications(); } } else { // Deliver the message connection->sendApplicationMessage(message); } } else { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "No switchboard available to deliver message," << " requesting chat and queueing message " << (pendingMimeMessages_.count() + 1) << "." << endl; #endif emit requestSwitchboard( handle, ChatInformation::CONNECTION_BACKGROUND ); queueMessage( message, handle, privateChatRequired ); } } // Parse an gif ink message void ChatMaster::slotGotInkMessage( const QString &ink, const QString &contactHandle, InkFormat format ) { #ifdef KMESSDEBUG_SWITCHBOARD_GENERAL switch( format ) { case FORMAT_ISF: kmDebug() << "Parsing ISF ink message"; break; case FORMAT_GIF: kmDebug() << "Parsing GIF ink message"; break; } #endif // Get the contact data for the shown messages QString friendlyName; QString contactPicture; const ContactBase *contact = currentAccount_->getContactByHandle( contactHandle ); if( contact != 0 ) { friendlyName = contact->getFriendlyName( STRING_ORIGINAL ); contactPicture = contact->getContactPicturePath(); } #if KMESS_ENABLE_INK == 0 // Receiving Ink is supported only for GIF, as GIF reading is always built-in into Qt // and KHTML can show GIF images without any extra libs if( format != FORMAT_GIF ) { showSpecialMessage( ChatMessage( ChatMessage::TYPE_SYSTEM, ChatMessage::CONTENT_SYSTEM_ERROR, true, i18nc( "Error message shown in chat, %1 is the name of the contact", "You received an handwritten message from %1, " "but it could not be displayed. This version of " "KMess was built without ISF support.", friendlyName ), contactHandle, friendlyName ) ); return; } #endif // Validate the incoming data QString inkData = ink.trimmed(); QRegExp inkChars( "([^A-Za-z0-9:+/=])" ); if( ! inkData.startsWith( "base64:" ) || inkChars.indexIn( inkData ) != -1 ) { kmWarning() << "Received invalid ink message from" << contactHandle << ", found char 0x" << QString::number( inkChars.cap(1)[0].unicode(), 16 ) << "at position" << inkChars.pos(1); kmWarning() << "Ink data dump:" << inkData; showSpecialMessage( ChatMessage( ChatMessage::TYPE_SYSTEM, ChatMessage::CONTENT_SYSTEM_ERROR, true, i18nc( "Error message shown in chat, %1 is the name of the contact", "You received an handwritten message from %1, " "but it could not be displayed. The data could " "not be read.", friendlyName ), contactHandle, friendlyName ) ); return; } QString imageMimeType; QByteArray inkEncodedData( inkData.toLatin1() ); inkEncodedData.replace( "base64:", "" ); switch( format ) { case FORMAT_ISF: { #if KMESS_ENABLE_INK == 1 // ISF messages need to be base64-decoded, converted to a picture readable by KHTML, // then base64-encoded again. // Since I'd rather avoid using giflib to encode ISF data into pictures, I'll use PNG. imageMimeType = "image/png"; Isf::Drawing drawing( Isf::Stream::reader( inkEncodedData, true /* fromBase64 */ ) ); inkEncodedData = Isf::Stream::writerPng( drawing, true ); #endif break; } case FORMAT_GIF: // Yes, this is it, just set the GIF mimetype. The data is already // there in the inkEncodedData variable, and KHTML can show inline // gif images right away imageMimeType = "image/gif"; break; } // Use the ink data as inline image // Cast the sender object to determine if the ink is received from p2p or switchboard. // WLM sends the ink (gif) by p2p protocol for private chat and by mime for multi-chat // Other clients send all by mime message, so it's important to determine what switchboard // is used, to find the correct chat window where to show the message. MsnSwitchboardConnection *switchboard = qobject_cast<MsnSwitchboardConnection *>( sender() ); // Send the message as HTML to the chat window. // Using data url scheme for embedded images (src="data:image/gif;base64,fdsfsdfsd") showSpecialMessage( ChatMessage( ChatMessage::TYPE_INCOMING, ChatMessage::CONTENT_MESSAGE_INK, true, "<img src=\"data:" + imageMimeType + ";" "base64," + Qt::escape( inkEncodedData ) + "\">", contactHandle, friendlyName, contactPicture ), switchboard ); } // The switchboard received a Mime message void ChatMaster::slotGotMessage(const MimeMessage &message, const QString &handle) { ApplicationList *appList = getApplicationList(handle); if( appList != 0 ) { appList->gotMessage(message); } } // The switchboard received a P2P message void ChatMaster::slotGotMessage(const P2PMessage &message, const QString &handle) { ApplicationList *appList = getApplicationList(handle); if( appList != 0 ) { appList->gotMessage(message); } } // The switchboard received an msn object void ChatMaster::slotGotMsnObject(const QString &msnObjectData, const QString &handle) { // Get the contact. ContactBase *contact = currentAccount_->getContactByHandle(handle); if(KMESS_NULL(contact)) return; // Get the switchboard connection where the emoticon/wink/voiceclip was posted const MsnSwitchboardConnection *connection = static_cast<const MsnSwitchboardConnection*>( sender() ); Chat *chat = 0; if( connection != 0 ) { chat = getChatBySwitchboard( connection ); } // Parse the msn object MsnObject msnObject(msnObjectData); // Get the contact name QString friendlyName( currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED_ESCAPED ) ); // Determine the statusmessage to display. QString statusMessage; MsnObject::MsnObjectType objectType = msnObject.getType(); if( objectType == MsnObject::WINK ) { statusMessage = i18n( "%1 is sending a wink: "%2"", friendlyName, msnObject.getFriendly() ); } // Show the status message if( ! statusMessage.isEmpty() ) { if( ! KMESS_NULL(connection) && ! KMESS_NULL(chat) ) { chat->getChatWindow()->showStatusMessage( statusMessage ); } } startMsnObjectDownload( handle, &msnObject, chat ); } // A msn object (picture, wink, emoticon) was received for the contact. void ChatMaster::slotMsnObjectReceived(const QString &handle, const MsnObject &msnObject) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Received msn object from: " << handle << "."; #endif Chat *chat = 0; // Get sender of this signal, slotGotMsnObject() assigned this to the p2papp. const P2PApplication *application = static_cast<const P2PApplication*>( sender() ); if( application != 0 ) { chat = application->getChat(); if( chat != 0 && ! chats_.contains( chat ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Original chat not found for received MSNObject " << "(objecttype=" << msnObject.getType() << " contact=" << handle << ")." << endl; #endif chat = getContactsChat( handle, true ); } } showMsnObject( handle, msnObject, chat ); } // The switchboard is ready to send more messages. void ChatMaster::slotSwitchboardReady() { // No need to send pending messages or resume an application (a message is always queued before the app is paused). if( pendingMimeMessages_.isEmpty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "A switchboard is ready to send more messages, no messages pending."; #endif return; } // Get the connection MsnSwitchboardConnection *connection = static_cast<MsnSwitchboardConnection*>( const_cast<QObject*>( sender() ) ); if(KMESS_NULL(connection)) return; // Get the contacts const QStringList &contacts = connection->getContactsInChat(); bool isPrivateChat = (contacts.size() < 2); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "A switchboard is ready to send more messages, " << pendingMimeMessages_.count() << " messages pending."; #endif // Send all pending messages sendPendingMimeMessages( contacts[0], connection ); // See if the switchboard is still ready. // Don't use connection->isBusy() as it will break with // the test above when exactly all messages are sent before the connection became busy. if( ! pendingMimeMessages_.isEmpty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Switchboard is busy again, not notifying applications."; #endif return; } // A switchboard is still available. // If the contact applications were paused, resume them now. ContactBase *contact = currentAccount_->getContactByHandle( contacts[0] ); if( contact != 0 && contact->hasApplicationList() ) { contact->getApplicationList()->resumeApplications(isPrivateChat); } } // Delete an existing switchboard void ChatMaster::slotSwitchboardDelete( MsnSwitchboardConnection *closing, bool deleteObject ) { // Remove from collection switchboardConnections_.removeAll( closing ); // Do not delete the object: this method can be called as slot from the switchboard's destructor if( ! deleteObject ) { return; } // Disconnect and destroy disconnect( closing, 0, this, 0 ); closing->deleteLater(); } // Configure and start the Mime application object. void ChatMaster::startApplication( MimeApplication *application ) { // Get the application list. ApplicationList *appList = getApplicationList( application->getContactHandle() ); if(KMESS_NULL(appList)) return; // Add to list appList->addApplication( application ); // requires MimeApplication/P2PApplication type. // Start application application->start(); } // Configure and start the P2P application object. void ChatMaster::startApplication( P2PApplication *application ) { // Get the application list. ApplicationList *appList = getApplicationList( application->getContactHandle() ); if(KMESS_NULL(appList)) return; // Add to list. appList->addApplication( application ); // requires MimeApplication/P2PApplication type. // Start application application->start(); } // Start a connection with the information gathered from the Notification connection 01594 MsnSwitchboardConnection *ChatMaster::startSwitchboard( const ChatInformation &chatInfo ) { MsnSwitchboardConnection *switchboard = 0; const QString &handle = chatInfo.getContactHandle(); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Starting new switchboard session with" << handle; #endif /** * Refresh requests are made by expired/disconnected switchboard sessions, and they are refreshed * with the new connection data. * All other types of chat request create a new switchboard session from the chat info. * We could attach the newly created SB to a chat window, but we don't know what kind of chat it * may be at this point (the list of initial participants will be received later, when the SB * connection will be established). Therefore, we just don't attach it to any chat, and so we * posticipate the decision to later. * (Also remember that you may have more than one chat with a contact, a 1-on-1, a group one, * and a data transfer one) */ if( chatInfo.getType() == ChatInformation::CONNECTION_REFRESH ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Finding the session to refresh."; #endif foreach( MsnSwitchboardConnection *currentSwitchboard, switchboardConnections_ ) { if( currentSwitchboard->getContactsInChat().contains( handle ) && currentSwitchboard->isWaiting() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Session found, it will be refreshed."; #endif switchboard = currentSwitchboard; break; } } if( switchboard == 0 ) { kmWarning() << "Unable to find which session requested a refresh."; return 0; } } else { switchboard = createSwitchboardConnection(); if( KMESS_NULL(switchboard) ) return 0; #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Creating new session."; #endif } // If this switchboard is a background one, made to request a contact's picture, // we can remove the pending request from the list now. if( chatInfo.getType() == ChatInformation::CONNECTION_BACKGROUND && pendingDisplayPictures_.contains( handle ) ) { pendingDisplayPictures_.removeAll( handle ); } // Connect to the new server switchboard->start( chatInfo ); Chat *chat = getChatBySwitchboard( switchboard ); if( chat ) { chat->setEnabled( true ); } return switchboard; } // Start a chat with the information gathered from a switchboard connection Chat *ChatMaster::createChat( MsnSwitchboardConnection *switchboard, bool requestedByUser ) { #ifdef KMESSTEST KMESS_ASSERT( switchboard != 0 ); #endif Chat *newChat; ChatWindow *newChatWindow; // If the new chat is a group chat, find existing group chat windows; else find private chats const QStringList partecipants( switchboard->getContactsInChat() ); newChat = getContactsChat( partecipants, true ); // If the chat was requested by a contact, check if we have a chat request // pending for that contact and remove it if( ! requestedByUser && partecipants.count() < 2 ) { const QString& handle = partecipants.first(); // Also change the request source to the user if we asked a chat with this contact first requestedByUser = requestedChats_.contains( handle ); if( requestedByUser ) { // Remove the request requestedChats_.removeAll( handle ); } } if( newChat != 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "A chat with contacts" << switchboard->getContactsInChat().join(",") << "already exists, raising it." << endl; #endif // The chat may have been disabled, reenable it just in case. // Seen happening with group chats where you get reinvited to after a reconnection. newChat->setEnabled( true ); // Delete the previous switchboard ( if it's different from the current one ) and attach the new MsnSwitchboardConnection *oldConnection = newChat->getSwitchboardConnection(); if( oldConnection != switchboard ) { newChat->setSwitchboardConnection( switchboard ); slotSwitchboardDelete( oldConnection, true ); } // If we've requested a chat window, raise it forcing it open over other KMess' windows; // if some contact wants to chat with us, the chat window will open minimized. raiseChat( newChat, requestedByUser ); return newChat; } // Create the chat widget newChat = new Chat(); if( newChat == 0 ) { kmDebug() << "Couldn't create a new chat tab."; return 0; } // Initialize it if( ! newChat->initialize( switchboard ) ) { kmDebug() << "Couldn't initialize the chat tab."; return 0; } // Select the chat window where to open the new tab, or create a new one newChatWindow = createChatWindow( newChat ); // Create the first tab in the chat newChat = newChatWindow->addChatTab( newChat, switchboard->getUserStartedChat() ); if( newChat == 0 ) { kmWarning() << "Couldn't add the initial chat."; return 0; } // Connect the chat's request signals connect( newChat, SIGNAL( gotChatMessage(const ChatMessage&,Chat*) ), this, SIGNAL( newChatMessage(const ChatMessage&,Chat*) ) ); connect( newChat, SIGNAL( contactAllowed(QString) ), this, SIGNAL( allowContact(QString) ) ); connect( newChat, SIGNAL( addContact(QString) ), this, SIGNAL( addContact(QString) ) ); connect( newChat, SIGNAL( contactAdded(QString,bool) ), this, SLOT ( forwardContactAdded(QString,bool) ) ); connect( newChat, SIGNAL( contactBlocked(QString,bool) ), this, SLOT ( forwardContactBlocked(QString,bool) ) ); connect( newChat, SIGNAL( startPrivateChat(const QString&) ), this, SLOT ( requestChat(QString) ) ); connect( newChat, SIGNAL( appCommand(QString,QString,QString) ), this, SLOT ( slotDeliverAppCommand(QString,QString,QString) ) ); connect( newChat, SIGNAL( requestFileTransfer(const QString&,const QString&) ), this, SLOT ( startFileTransfer(const QString&,const QString&) ) ); connect( newChat, SIGNAL( closing(Chat*) ), this, SLOT ( slotChatClosing(Chat*) ) ); connect( newChat, SIGNAL( destroyed(QObject*) ), this, SLOT ( slotChatDestroyed(QObject*) ) ); connect( this, SIGNAL( updateApplicationMessage(const QString&,const QString&) ), newChat, SIGNAL( updateApplicationMessage(const QString&,const QString&) ) ); // Add the new chat to our list chats_.append( newChat ); // Finally, start the new chat newChat->startChat(); // If we've requested a chat window, raise it forcing it open over other KMess' windows; // if some contact wants to chat with us, the chat window will open minimized. raiseChat( newChat, requestedByUser ); return newChat; } // Create a new chat window or retrieve an existing one if tabbed chatting is enabled ChatWindow *ChatMaster::createChatWindow( Chat *chat ) { // Search a window for the chat according to the users grouping settings ChatWindow *window = findWindowForChat( chat ); // Found a window to use, return that if( window != 0 ) { return window; } // Create the new chat window #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "No existing chat window, creating new."; #endif // Create the window: it should have no parent, so it will be displayed in the taskbar for Windows. window = new ChatWindow(); // Bail out if the window could not be initialized if( ! window->initialize() ) { kmWarning() << "Couldn't initialize the new chat window!"; return 0; } // Connect its signals connect( window, SIGNAL( destroyed(QObject*) ), this, SLOT ( slotChatWindowDestroyed(QObject*) ) ); connect( window, SIGNAL( reconnect() ), this, SIGNAL( reconnect() ) ); // Add the new chat window to our list chatWindows_.append( window ); return window; } // Create and register a new switchboard MsnSwitchboardConnection *ChatMaster::createSwitchboardConnection( MsnSwitchboardConnection *replace, QString handle ) { MsnSwitchboardConnection *switchboard; #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Creating a new switchboard connection."; #endif // Initialize the switchboard connection if( replace != 0 ) { switchboard = new MsnSwitchboardConnection( *replace ); } else { switchboard = new MsnSwitchboardConnection(); } if( ! switchboard || ! switchboard->initialize( handle ) ) { kmWarning() << "Couldn't initialize switchboard connection."; delete switchboard; return 0; } switchboardConnections_.append( switchboard ); // Connect the switchboard's signals to the Chat Master. connect(switchboard, SIGNAL( contactJoinedChat(ContactBase*) ), this, SLOT ( slotContactJoinedChat(ContactBase*) )); connect(switchboard, SIGNAL( gotMessage(const MimeMessage&, const QString&) ), this, SLOT ( slotGotMessage(const MimeMessage&, const QString&) )); connect(switchboard, SIGNAL( gotMessage(const P2PMessage&, const QString&) ), this, SLOT ( slotGotMessage(const P2PMessage&, const QString&) )); connect(switchboard, SIGNAL( gotInkMessage(const QString&, const QString&,InkFormat) ), this, SLOT ( slotGotInkMessage(const QString&, const QString&,InkFormat) )); connect(switchboard, SIGNAL( gotMsnObject(const QString&, const QString&) ), this, SLOT ( slotGotMsnObject(const QString&, const QString&) )); connect(switchboard, SIGNAL( readySend() ), this, SLOT ( slotSwitchboardReady() )); connect(switchboard, SIGNAL( requestNewSwitchboard(QString) ), this, SLOT ( forwardRequestNewSwitchboard(QString) )); connect(switchboard, SIGNAL( requestChatWindow(MsnSwitchboardConnection*) ), this, SLOT ( createChat(MsnSwitchboardConnection*) )); connect(switchboard, SIGNAL( deleteMe(MsnSwitchboardConnection*) ), this, SLOT ( slotSwitchboardDelete(MsnSwitchboardConnection*) )); return switchboard; } // Start a file transfer with the information from the Chat void ChatMaster::startFileTransfer( const QString &handle, const QString &filename ) { ApplicationList *appList = getApplicationList(handle); if(KMESS_NULL(appList)) return; // Get the contact properties, see how we can transfer the file. const ContactBase *contact = currentAccount_->getContactByHandle( handle ); // If the contact is offline (that is, not online nor invisible), refuse to start the file transfer MsnSwitchboardConnection *connection = getContactSwitchboardConnection( handle, true ); if( ! connection || ( contact && contact->isOffline() && connection->isInactive() ) ) { // kmWarning() << "Attempted to start a file transfer with an offline contact!"; return; } if( contact == 0 || ! contact->hasP2PSupport() ) { // This should be so rare that it justifies a warning message. if( contact == 0 ) { kmWarning() << "Contact" << handle << "object not found, using old MIME invitations because P2P might not be supported."; } else { kmWarning() << "Contact" << handle << "does not support P2P transfers, using old MIME invitations instead."; } // The contact only supports file transfer the old way MimeApplication *app = new FileTransfer(handle, filename); startApplication(app); return; } #if CHATMASTER_SEND_FILES_MSNFTP kmWarning() << "Sending files is forced over MSNFTP for debugging, using old MIME invitations."; MimeApplication *app = new FileTransfer(handle, filename); startApplication(app); #else // The contact supports file transfer over MSNP2P P2PApplication *app = new FileTransferP2P(appList, filename); startApplication(app); #endif } // Start a chat and send a file to the contact void ChatMaster::startChatAndFileTransfer( const QString &handle, const QString &filename ) { requestChat( handle ); int i = 0; // FIXME: Workaround needed, because the filetransfer is "lost" when the Switchboard is not open yet. // suggested workarround: passing an "initial action" to requestChat(). while( !getContactSwitchboardConnection( handle, true ) && i++ < 75 ) { QTest::qWait(200); } startFileTransfer( handle, filename ); } // Start a msnobject transfer to get the contact's msn object. void ChatMaster::startMsnObjectDownload( const QString &handle, const MsnObject *msnObject, Chat *chat ) { // Test input if(KMESS_NULL(msnObject)) return; // Get the application list ApplicationList *appList = getApplicationList(handle); if(KMESS_NULL(appList)) return; // First check if the actual object is currently being downloaded already. // call before cache check, since file can be partially downloaded. if( appList->hasMsnObjectTransfer(*msnObject) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "object is already being downloaded, not sending a second invite."; #endif return; } // Get the picture filename, perhaps from a cache QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( *msnObject ) ); bool fileExists = QFile::exists( objectFileName ); // Check if the image can be read. ( TODO this step is skipped for WINK for the moment ) if( fileExists && msnObject->getType() != MsnObject::WINK ) { bool imageCorrupt = QImageReader::imageFormat( objectFileName ).isEmpty(); if( imageCorrupt || ! msnObject->verifyFile( objectFileName ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Cached MsnObject is corrupt " << ( imageCorrupt ? "(detected by QImage)" : "(detected by MsnObject)" ) << ", deleting it: '" << objectFileName << "'" << endl; #endif QFile::remove( objectFileName ); fileExists = false; } } // Avoid downloading again if it exists if( fileExists ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Contact MsnObject is already in cache."; #endif // Already have it, handle processing in a generic way. // Don't use slotMsnObjectReceived() because it uses sender() to get the Chat object. showMsnObject( handle, msnObject->objectString(), chat ); } else { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Starting MsnObject download (type=" << msnObject->getType() << ")."; #endif // Create and initialize the application. P2PApplication *app = new MsnObjectTransferP2P(appList, *msnObject); app->setChat( chat ); // for winks, to display in originating chat window. connect(app, SIGNAL( msnObjectReceived(const QString&, const MsnObject&) ), this, SLOT ( slotMsnObjectReceived(const QString&, const MsnObject&) )); startApplication(app); } } /** * Periodically called method for update commands. * * This method is synchronized with the ping timer of MsnNotificationConnection * to avoid multiple timers which reduces power consumption (see www.linuxpowertop.org). * * Currently this is used to download display pictures in the background, * but it can also be used for other events. */ 02023 void ChatMaster::timedUpdate() { // Handle the queue of display pictures to download, but only if we're not hidden if( pendingDisplayPictures_.count() == 0 || currentAccount_->getStatus() == STATUS_INVISIBLE ) { return; } #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "handling timed events."; #endif // See if there are pictures to download. // Get 4 pictures at once. int requestCount = pendingDisplayPictures_.count(); requestCount = requestCount > 4 ? 4 : requestCount; while( requestCount > 0 && ! pendingDisplayPictures_.isEmpty() ) { // Unshift the first handle of the list. QString handle( pendingDisplayPictures_.first() ); pendingDisplayPictures_.removeAll( handle ); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "see if the display picture of '" << handle << "' should be downloaded."; #endif // See if the picture was already downloaded somehow (e.g. user opened a chat) // First get the contact. // Using getContactList()->.. because it skips the InvitedContact objectss returned by CurrentAccount::getContactByHandle() Contact *contact = currentAccount_->getContactList()->getContactByHandle( handle ); if( contact == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "contact not found, was it removed?"; #endif continue; } // See if the contact is still online. if( contact->isOffline() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "contact is no longer online."; #endif continue; } // See if the contact is still blocked. // Avoid starting a chat which "invites" the contact to chat back. if( contact->isBlocked() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "contact is blocked."; #endif continue; } // Get the msn object, can become null now. const MsnObject *msnObject = contact->getMsnObject(); if( msnObject == 0 ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "MsnObject has been reset."; #endif continue; } // See if the object already exists in the cache QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( *msnObject ) ); if( QFile::exists( objectFileName ) && ! QImageReader::imageFormat( objectFileName ).isEmpty() && msnObject->verifyFile( objectFileName ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Picture is already downloaded."; #endif continue; } // See if the actual object is currently being downloaded (e.g. user just opened a chat). // Put as last check because it creates the ApplicationList on demand. #ifdef KMESSDEBUG_CHATMASTER // Made the debug output easier to understand. if( ! contact->hasApplicationList() ) { kmDebug() << "Picture is not available, creating application list."; } #endif // Get the application list ApplicationList *appList = getApplicationList( handle ); if( KMESS_NULL(appList) ) continue; // See if a picture is currently being downloaded. if( appList->hasMsnObjectTransfer( *msnObject ) ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "transfer for the picture is already active."; #endif continue; } // All tests passed. // Download the picture. #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Picture is not available, starting switchboard connection to download it."; #endif // Start a chat to download the picture. // The download will be initiated up by ChatMaster::slotContactJoinedChat(). emit requestSwitchboard( handle, ChatInformation::CONNECTION_BACKGROUND ); requestCount--; } } // Update the grouping of chats/tabs. void ChatMaster::updateChatGrouping() { if( currentAccount_->getTabbedChatMode() == chatTabbedMode_ ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Chat grouping mode not changed, nothing to update"; #endif return; } #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Updating Chat grouping"; #endif chatTabbedMode_ = currentAccount_->getTabbedChatMode(); if( chatWindows_.empty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "No ChatWindows, nothing to update"; #endif return; } ChatWindow *currentWindow; ChatWindow *window; foreach( Chat *chat, chats_ ) { currentWindow = chat->getChatWindow(); window = findWindowForChat( chat); if( window == 0) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "No suited window found for chat" << chat; #endif ChatWindow* newChatWindow = createChatWindow( chat); newChatWindow->addChatTab( chat, false ); currentWindow->removeChatTab( chat); currentWindow->checkAndCloseWindow(); raiseChat( chat, false ); } else if( window != currentWindow ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Moving chat" << chat << "to other window" << window; #endif window->addChatTab( chat, false ); currentWindow->removeChatTab( chat); currentWindow->checkAndCloseWindow(); raiseChat( chat, false ); } } } // Search the appropriate window for a chat. ChatWindow *ChatMaster::findWindowForChat( Chat *chat ) { ChatWindow *window = 0; ChatWindow *otherGroupWindow = 0; CurrentAccount *currentAccount = currentAccount_; if( ! chatWindows_.empty() ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "At least one chat window is already open: checking the tabbed chatting settings."; #endif if( currentAccount->getTabbedChatMode() == 0 ) // Always group in tabs { // Pick the last open chat window window = chatWindows_.last(); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Always group in tabs. Picking the last created window."; #endif } else if( currentAccount->getTabbedChatMode() == 1 ) // Group tabs by initial contact's group { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Group in tabs by contact group. Choosing a window."; #endif QString handle( chat->getSwitchboardConnection()->getFirstContact() ); Contact *contact = currentAccount->getContactList()->getContactByHandle( handle ); if( contact && ! contact->getGroupIds().isEmpty() ) { QString group( contact->getGroupIds().first() ); #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Searching chats for one whose first contact is in the same group as the new one's."; #endif foreach( Chat *chatItem, chats_ ) { QString handle( chatItem->getSwitchboardConnection()->getFirstContact() ); Contact *contact = currentAccount->getContactList()->getContactByHandle( handle ); if( contact && ! contact->getGroupIds().isEmpty() && contact->getGroupIds().first() == group ) { #ifdef KMESSDEBUG_CHATMASTER kmDebug() << "Match found!"; #endif window = chatItem->getChatWindow(); // We found a possibly suited window. // Check if it already displays tabs of another group. // If so, we can't use it. if( otherGroupWindow == window ) { window = 0; } break; } else { // Keep track of the window used by another group. otherGroupWindow = chatItem->getChatWindow(); } } } } } return window; } #include "chatmaster.moc"