Files
kmess/src/chat/chatmaster.cpp
T
2026-07-06 09:37:59 +02:00

2274 lines
70 KiB
C++

/***************************************************************************
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 <KLocalizedString>
#include <kwindowsystem.h>
#include <kx11extras.h>
// 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 *>(QApplication::instance());
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 )
{
if( KWindowSystem::isPlatformX11() )
{
KX11Extras::setOnDesktop( chatWindow->winId(), KX11Extras::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();
}
// 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->windowHandle() );
chatWindow->activateWindow();
}
/**
* @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.
*/
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() );
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() );
}
}
}
// 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.
*/
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.
*/
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," + QString::fromLatin1( inkEncodedData ).toHtmlEscaped() + "\">",
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: &quot;%2&quot;", 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
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.
*/
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;
}