/*************************************************************************** msnobjecttransferp2p.cpp - description ------------------- begin : Fri Nov 26 2004 copyright : (C) 2004 by Diederik van der Boor email : vdboor --at-- codingdomain.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 "msnobjecttransferp2p.h" #include "../../currentaccount.h" #include "../../emoticonmanager.h" #include "../../kmessdebug.h" #include "../../utils/kmessconfig.h" #include "../../utils/kmessshared.h" #include "../mimemessage.h" #include "../p2pmessage.h" #include <QCryptographicHash> #include <QFile> #include <QFileInfo> #include <QImageReader> #include <QRegExp> /** * Constructor * * @param applicationList The shared sources for the contact. */ 00041 MsnObjectTransferP2P::MsnObjectTransferP2P(ApplicationList *applicationList) : P2PApplication(applicationList) , file_(0) , fileName_() { } /** * Constructor * * @param applicationList The shared sources for the contact. * @param msnObject MSNObject identifying the picture to request. */ 00056 MsnObjectTransferP2P::MsnObjectTransferP2P(ApplicationList *applicationList, const MsnObject &msnObject) : P2PApplication(applicationList) , file_(0) , msnObject_(msnObject) { } /** * Destructor, closes the file if it's open. */ 00068 MsnObjectTransferP2P::~MsnObjectTransferP2P() { if(file_ != 0) { delete file_; } } /** * Step one of a contact-started chat: the contact invites the user * * On error, send an 500 Internal Error back. The error will be acked and the application will terminate. * * @param message The invitation message */ 00085 void MsnObjectTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage &message) { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif // Extract the fields from the message unsigned long int appID = message.getValue("AppID").toUInt(); QString context( message.getValue("Context") ); // Extract the MSNObject from the context field, to determine which picture/emoticon the contact wants // Just to be on the safe side, check the buffer size before we start decoding if( context.length() <= 24 ) { kWarning() << "MSNObject transfer context field has bad formatting, " "ignoring invite (context=" << context << ", contact=" << getContactHandle() << ")."; sendCancelMessage( CANCEL_FAILED ); return; } // Decode the MSN Object contained in the Context field to a string // Decode that string to an MSN Object QByteArray decodedContext = QByteArray::fromBase64( context.toLatin1() ); QString contextString( decodedContext ); msnObject_ = MsnObject( contextString ); #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Got context \"" << contextString << "\"."; // Got context <msnobj Creator="contact@hotmail.com" Size="9442" Type="3" Location="KMess.tmp" Friendly="AA==" SHA1D="FyY3n97RHXDgQujca4FVMv4VIF0=" SHA1C="qsT1Tqmtt7Z0LucXOq1pd9p7cIE="/> #endif switch( msnObject_.getType() ) { case MsnObject::DISPLAYPIC: // Test protocol compatibility. if( appID != 1 && appID != 12 ) // Display pictures - AppID 12 is used as of MSN Messenger 7.5 { kDebug() << "Received a request for a display picture, but unexpected appID was set " "(appid=" << appID << " type=" << msnObject_.getType() << " contact=" << getContactHandle() << " action=continue)."; } // Continue at separate function contactStarted1_gotDisplayPictureRequest(); return; case MsnObject::EMOTICON: // Test protocol compatibility. if( appID != 1 // HACK: added for compatibility with Messenger for the Mac 6.0.3 and aMsn 0.97rc1 && appID != 11 ) // Custom emoticons { kDebug() << "Received a request for an emoticon, but unexpected appID was set " "(appid=" << appID << " type=" << msnObject_.getType() << " contact=" << getContactHandle() << " action=continue)."; } // Continue at separate function contactStarted1_gotEmoticonRequest(); return; // Avoid gcc warnings about missing values. // But don't use "default" so the check remains intact. case MsnObject::BACKGROUND: case MsnObject::DELUXE_DISPLAYPIC: break; case MsnObject::WINK: contactStarted1_gotWinkRequest(); return; default: break; } // Unknown application type kWarning() << "Received an invitation for an unexpected object type " "(appid=" << appID << " type=" << msnObject_.getType() << " contact=" << getContactHandle() << ")."; // Abort the contact. sendCancelMessage( CANCEL_FAILED ); } /** * Step one continued, the request is for the display picture. */ 00179 void MsnObjectTransferP2P::contactStarted1_gotDisplayPictureRequest() { // Send our display picture: check if the MSNObject the contact wants is the picture we've got. if( msnObject_.hasChanged( CurrentAccount::instance()->getMsnObjectString() ) ) { kWarning() << "Contact " << getContactHandle() << " wants a display picture we don't have!\n" "Requested object: " << msnObject_.objectString() << "\n" "Current object: " << CurrentAccount::instance()->getMsnObjectString() << "\n" "Aborting invite."; sendCancelMessage( CANCEL_FAILED ); return; } // The file to send is our picture fileName_ = CurrentAccount::instance()->getPicturePath(); // Reject because there is no file to send if( fileName_.isEmpty() ) { kWarning() << "Got an invitation, but we don't have a picture to send."; sendCancelMessage( CANCEL_FAILED ); return; } // Everything seems OK, accept this message contactStarted2_UserAccepts(); } /** * Step one continued, the request is for an emoticon. */ 00212 void MsnObjectTransferP2P::contactStarted1_gotEmoticonRequest() { QString themePath( EmoticonManager::instance()->getThemePath( true ) ); // Find if there is a picture named as the one the contact wants QFile pictureFile( themePath + msnObject_.getLocation() ); if( ! pictureFile.open( QIODevice::ReadOnly ) ) { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Couldn't open file: " << pictureFile.fileName(); #endif sendCancelMessage( CANCEL_FAILED ); return; } // Read the picture's data and create a MSNOject of it to test if it really is the requested picture QByteArray data = pictureFile.readAll(); pictureFile.close(); MsnObject testObject( CurrentAccount::instance()->getHandle(), msnObject_.getLocation(), QString::null, MsnObject::EMOTICON, data ); #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "msnObject_: " << msnObject_.objectString(); kDebug() << "testObject: " << testObject.objectString(); #endif // Test if they're the same picture if( msnObject_.hasChanged( testObject.objectString() ) ) { kWarning() << "Contact " << getContactHandle() << " wants a custom emoticon we don't have!" << "Requested object: " << msnObject_.objectString() << "Aborting invite."; sendCancelMessage( CANCEL_FAILED ); return; } #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Picture found, accepting."; #endif // Everything seems OK, accept this message fileName_ = pictureFile.fileName(); // Remember the name of the file to send contactStarted2_UserAccepts(); } // Step one continued, the request is for an wink. void MsnObjectTransferP2P::contactStarted1_gotWinkRequest() { fileName_ = KMessConfig::instance()->getMsnObjectFileName( msnObject_ ); #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Requested wink, sending: " + fileName_; #endif if( QFile::exists( fileName_ ) ) { contactStarted2_UserAccepts(); return; } // Abort the sending because the file doesn't exist sendCancelMessage( CANCEL_FAILED ); } /** * Step two of a contact-started chat: the user accepts. */ 00284 void MsnObjectTransferP2P::contactStarted2_UserAccepts() { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( file_ == 0 ); KMESS_ASSERT( ! fileName_.isEmpty() ); #endif // Now we try to open the file file_ = new QFile(fileName_); bool success = file_->open(QIODevice::ReadOnly); if( ! success ) { // Notify the user, even if debug mode is not enabled. kWarning() << "Unable to open file: " << fileName_ << "!"; // Close the file (also causes gotData() to fail) delete file_; file_ = 0; #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Cancelling session"; #endif // Send 500 Internal Error back if we failed // the error will be ACK-ed. sendCancelMessage( CANCEL_FAILED ); return; } #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Sending accept message"; #endif // Create the message MimeMessage message; message.addField( "SessionID", QString::number( getInvitationSessionID() ) ); // Send the message sendSlpOkMessage(message); } /** * Step three of a contact-started chat: the contact confirms the accept * * @param message The message of the other contact, not usefull in P2P sessions because it's an ACK. */ 00338 void MsnObjectTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage &/*message*/) { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif // Send the data preparation message back. // Once this message is ACKed, we can send our code sendDataPreparation(); } /** * Step four in a contact-started chat: the contact confirms the data preparation message. */ 00354 void MsnObjectTransferP2P::contactStarted4_ContactConfirmsPreparation() { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif // Send the file, the base class handles everything else here sendData(file_, P2P_TYPE_PICTURE); } /** * Return the application's GUID. */ 00369 QString MsnObjectTransferP2P::getAppId() { return "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"; } /** * Return the msn object of the picture we're transferring */ 00379 const MsnObject & MsnObjectTransferP2P::getMsnObject() const { return msnObject_; } /** * Called when data is received. * Writes the received data to the output file. * * @param message P2P message with the data. */ 00392 void MsnObjectTransferP2P::gotData(const P2PMessage &message) { if(file_ == 0) { kWarning() << "Unable to handle file data: no file open or already closed " << "(offset=" << message.getDataOffset() << " totalsize=" << message.getTotalSize() << " contact=" << getContactHandle() << ")!"; // Cancel if we can't receive it. // If this happens we're dealing with a very stubborn client, // because we already rejected the data-preparation message. sendCancelMessage(CANCEL_FAILED); return; } // Write the data in the file // Let the parent class do the heavy lifting, and abort properly. bool success = writeP2PDataToFile( message, file_ ); if( ! success ) { // Close the file file_->flush(); file_->close(); return; } // When all data is received, the parent class calls showTransferComplete(). // That method will test the file and send the msnObjectReceived() signal. } /** * Indicates a private chat is not required, overwritten from the base class. * Returns true by default, unless an emoticon is transferred, or the contact is not in the list. * * @returns Returns true if a private chat is required for this application. */ 00431 bool MsnObjectTransferP2P::isPrivateChatRequired() const { // MsnObject transfer can run in a multi-chat too, // The P2P-Dest field of the p2p messages make sure other participants ignore them. // For emoticon transfers, a separate private chat is not required, // for larger transfers (winks), it's recommended to use a private chat. // When the contact is not in our contact list, it may be possible no new chat can be made, so don't enforce this. MsnObject::MsnObjectType type = msnObject_.getType(); bool contactInList = CurrentAccount::instance()->hasContactInList( getContactHandle() ); return (type != MsnObject::EMOTICON && contactInList); } /** * Hide standard informative application message (e.g. user invited, cancelled). * * Avoid annoying messages in the chat windows about "Contact sent something KMess does not support". * This is useful for interactive invitations, like file transfer. Since msnobject transfer happen * in the background, it's not helping to have empty chat windows popping up with error messages. */ 00453 void MsnObjectTransferP2P::showEventMessage(const QString &message, const ChatMessage::ContentsClass contents, bool isIncoming ) { Q_UNUSED( isIncoming ); kWarning() << "suppressed message:" << message << "(contact=" << getContactHandle() << ", contentsClass=" << contents << ")"; } /** * Show a message to notify about a system error. * * Avoid annoying messages in the chat windows. See showEventMessage(). */ 00467 void MsnObjectTransferP2P::showSystemMessage( const QString &message, const ChatMessage::ContentsClass contents, bool isIncoming ) { Q_UNUSED( isIncoming ); kWarning() << "suppressed message:" << message << "(contact=" << getContactHandle() << ", contentsClass=" << contents << ")"; } /** * Called when the transfer is complete. * Closes the file, verifies the MsnObject and updates possible listeners. */ 00480 void MsnObjectTransferP2P::showTransferComplete() { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "Last data part received, closing file"; #endif if( KMESS_NULL(file_) ) return; bool success = false; QString pictureFileName( file_->fileName() ); // Don't send an ACK here, it's already ACK-ed // (with a special BYE-request ACK) // Clean up file_->flush(); file_->close(); // Check if the received image is valid. Will cost some CPU though. QString fileHash( KMessShared::generateFileHash( pictureFileName ).toBase64() ); success = ( msnObject_.getDataHash() == fileHash ); // Clean up delete file_; file_ = 0; QFileInfo fileInfo; QString ext; // Special checks for some types. switch( msnObject_.getType() ) { case MsnObject::DISPLAYPIC: // Auto-assign the extension for the file ext = QImageReader::imageFormat( pictureFileName ); if( ext.isEmpty() || ! success ) { // If the type of image isn't support or the data is corrupted // remove the file QFile::remove ( pictureFileName ); return; } // Rename the file with the new extension if there is anyone fileInfo.setFile( pictureFileName ); if( fileInfo.suffix().isEmpty() ) { QFile::rename( pictureFileName, pictureFileName + "." + ext ); } break; case MsnObject::BACKGROUND: case MsnObject::EMOTICON: // For pictures, see if it's broken. if( QImageReader::imageFormat( pictureFileName ).isEmpty() || ! success ) { kWarning() << "Received image was broken (contact=" << getContactHandle() << ")."; QFile::remove( pictureFileName ); return; } break; case MsnObject::WINK: { QFile file; // Write the wink friendly to a file. // The file will be used to retrieve the actual wink name, ie "Frog" file.setFileName( pictureFileName + ".name" ); if( file.open( QIODevice::WriteOnly | QIODevice::Text ) ) { QTextStream out( &file ); out << msnObject_.getFriendly(); out.flush(); file.close(); } // Read the stamp value, there is the certificate of the wink // This is a X509 certificate, signed by the Microsoft // If this value is missing, we can show the wink anyway and be able to sent it to no WLM clients const QString& stamp( msnObject_.getAttribute( QString( "stamp" ), msnObject_.objectString() ) ); if( stamp.isEmpty() ) { break; } // Write the stamp value to the file. Note that if we don't write this value // we can't send the wink to WLM, because it checks for the certificate file.setFileName( pictureFileName + ".stamp" ); if ( ! file.open(QIODevice::WriteOnly | QIODevice::Text ) ) { break; } QTextStream out(&file); out << stamp; out.flush(); file.close(); break; } // Avoid gcc warnings. default: break; } // Send an event to the switchboard: emit msnObjectReceived( getContactHandle(), msnObject_ ); // The application should close automatically now, // and it sends the BYE message automatically too. } /** * Hide transfer messages, by overwriting the default method implementation. */ 00599 void MsnObjectTransferP2P::showTransferMessage( const QString &message ) { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "suppressed message: " << message; #else Q_UNUSED( message ); // Avoid compiler warnings #endif // WLM8 appears to initiate direct connections for msnobject transfers as well. // this method hides the connecting-messages by simply overwriting the base method and no nothing instead. } /** * Step one of a user-started chat: the user invites the contact */ 00616 void MsnObjectTransferP2P::userStarted1_UserInvitesContact() { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug() << "requesting display picture"; #endif // Set the filename fileName_ = KMessConfig::instance()->getMsnObjectFileName(msnObject_); // Encode the context // The \0 char is added on purpose, this fixes the msnobject transfers with Bot2k3. // Sometimes the context string is padded with '=' characters, note this is a feature of BASE64 encoding! QString context; QByteArray rawObject = msnObject_.objectString().toUtf8(); rawObject.append( '\0' ); // appears to be sent by the official client, likely unintentional. context = rawObject.toBase64(); int appId; MsnObject::MsnObjectType objectType = msnObject_.getType(); switch( objectType ) { case MsnObject::DISPLAYPIC: appId = 12; break; case MsnObject::WINK: appId = 17; break; case MsnObject::EMOTICON: appId = 11; break; case MsnObject::BACKGROUND: appId = 2; default: appId = 1; // the old appId for msn object transfers } // Send the invitation sendSlpSessionInvitation( KMessShared::generateID(), getAppId(), appId, context); } /** * Step two of a user-started chat: the contact accepts * * @param message Accept message of the other contact */ 00667 void MsnObjectTransferP2P::userStarted2_ContactAccepts(const MimeMessage & /*message*/) { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif // We don't need to do anything else here. // The contact still needs to send the data preparation. // Meanwhile, the base class acks the "SLP/200 OK" message automatically // with the session ID we gave in the sendSlpSessionInvitation() } /** * Step three of a user-started chat: the user prepares for the session. */ 00684 void MsnObjectTransferP2P::userStarted3_UserPrepares() { #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( file_ == 0 ); KMESS_ASSERT( ! fileName_.isEmpty() ); #endif if( file_ != 0 ) { // TODO: Quick fix for WML8, this method is called twice because WLM8 // initializes a Direct connection while it sent the data preparation #ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P kWarning() << "this method was already called before."; #endif return; } file_ = new QFile(fileName_); bool success = file_->open(QIODevice::WriteOnly); if( ! success ) { // Notify the user, even if debug mode is not enabled. kWarning() << "Unable to open file: " << fileName_ << "!"; // Close the file (also causes gotData() to fail) delete file_; file_ = 0; return; } // Acknowledge the data-preparation message // Final step is the gotData() handling.. sendDataPreparationAck(); } #include "msnobjecttransferp2p.moc"