Logo Search packages:      
Sourcecode: kmess version File versions  Download package

picturetransferp2p.cpp

/***************************************************************************
                          picturetransferp2p.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 "picturetransferp2p.h"

#include "../mimemessage.h"
#include "../p2pmessage.h"
#include "../../kmessdebug.h"
#include "../../crypt/sha1.h"
#include "../../currentaccount.h"
#include "../../emoticonmanager.h"

#include <qcstring.h>
#include <qfile.h>
#include <qregexp.h>
#include <qpixmap.h>

#include <kdebug.h>
#include <kstandarddirs.h>
#include <kmdcodec.h>


/**
 * Constructor
 *
 * @param  applicationList  The shared sources for the contact.
 */
00042 PictureTransferP2P::PictureTransferP2P(ApplicationList *applicationList)
    : P2PApplication(applicationList)
    , file_(0)
    , fileName_(QString::null)
{
}



/**
 * Constructor
 *
 * @param  applicationList  The shared sources for the contact.
 * @param  msnObject        MSNObject identifying the picture to request.
 */
00057 PictureTransferP2P::PictureTransferP2P(ApplicationList *applicationList, const MsnObject &msnObject)
    : P2PApplication(applicationList)
    , file_(0)
    , msnObject_(msnObject)
{
}



/**
 * Destructor, closes the file if it's open.
 */
00069 PictureTransferP2P::~PictureTransferP2P()
{
  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
 */
00086 void PictureTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage &message)
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::contactStarted1_ContactInvitesUser()" << endl;
#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 )
  {
    kdWarning() << "PictureTransferP2P::contactStarted1_ContactInvitesUser() - "
                   "Picture transfer context field has bad formatting, "
                   "ignoring invite (context=" << context << ", contact=" << getContactHandle() << ")." << endl;
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // Decode the MSN Object contained in the Context field to a string
  QByteArray encodedContext, decodedContext;
  encodedContext.duplicate( context.data(), context.length() );
  KCodecs::base64Decode( encodedContext, decodedContext );

  // Decode that string to an MSN Object
  QString contextString( decodedContext );
  msnObject_ = MsnObject( contextString );

  MsnObject testObject;

#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::contactStarted1_ContactInvitesUser() - Got context \"" << contextString << "\"." << endl;
// 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
      {
        kdDebug() << "PictureTransferP2P::contactStarted1_ContactInvitesUser() - "
                      "Received a request for a display picture, but unexpected appID was set "
                      "(appid=" << appID <<
                      " type=" << msnObject_.getType() <<
                      " contact=" << getContactHandle() << " action=continue)." << endl;
      }

      // 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
      {
        kdDebug() << "PictureTransferP2P::contactStarted1_ContactInvitesUser() - "
                     "Received a request for an emoticon, but unexpected appID was set "
                     "(appid=" << appID <<
                     " type=" << msnObject_.getType() <<
                     " contact=" << getContactHandle() << " action=continue)." << endl;
      }

      // 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:
    case MsnObject::WINK:
    case MsnObject::VOICECLIP:
    case MsnObject::ROAMING_OBJECT:
      break;
  }

  // Unknown application type
  kdWarning() << "PictureTransferP2P::contactStarted1_ContactInvitesUser() - "
                  "Received an invitation for an unexpected object type "
                  "(appid=" << appID <<
                  " type=" << msnObject_.getType() <<
                  " contact=" << getContactHandle() << ")." << endl;

  // Abort the contact.
  sendCancelMessage( CANCEL_FAILED );
}



/**
 * Step one continued, the request is for the display picture.
 */
00187 void PictureTransferP2P::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() ) )
  {
    kdWarning() << "PictureTransferP2P::contactStarted1_gotDisplayPictureRequest() - "
                   "Contact " << getContactHandle() << " wants a display picture we don't have!\n"
                   "Requested object: " << msnObject_.objectString() << "\n"
                   "Current object:   " << CurrentAccount::instance()->getMsnObjectString() << "\n"
                   "Aborting invite." << endl;
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // The file to send is our picture
  fileName_ = CurrentAccount::instance()->getImagePath();

  // Reject because there is no file to send
  if( fileName_.isEmpty() )
  {
    kdWarning() << "PictureTransferP2P::contactStarted1_gotDisplayPictureRequest() - "
                   "Got an invitation, but we don't have a picture to send." << endl;
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // Everything seems OK, accept this message
  contactStarted2_UserAccepts();
}



/**
 * Step one continued, the request is for an emoticon.
 */
00222 void PictureTransferP2P::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( IO_ReadOnly ) )
  {
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
    kdDebug() << "PictureTransferP2P::contactStarted1_gotEmoticonRequest() - "
                  "Couldn't open file: " << pictureFile.name() << endl;
#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_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P: msnObject_: " << msnObject_.objectString() << endl;
  kdDebug() << "PictureTransferP2P: testObject: " << testObject.objectString() << endl;
#endif

  // Test if they're the same picture
  if( msnObject_.hasChanged( testObject.objectString() ) )
  {
    kdWarning() << "PictureTransferP2P::contactStarted1_gotEmoticonRequest() - Contact " << getContactHandle()
                << " wants a custom emoticon we don't have!" << endl
                << "Requested object: " << msnObject_.objectString() << endl
                << "Aborting invite." << endl;
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::contactStarted1_gotEmoticonRequest() - Picture found, accepting." << endl;
#endif

  // Everything seems OK, accept this message
  fileName_ = pictureFile.name();         // Remember the name of the file to send
  contactStarted2_UserAccepts();
}



/**
 * Step two of a contact-started chat: the user accepts.
 */
00274 void PictureTransferP2P::contactStarted2_UserAccepts()
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - contactStarted2_UserAccepts" << endl;
#endif

#ifdef KMESSTEST
  ASSERT(   file_ == 0          );
  ASSERT( ! fileName_.isEmpty() );
#endif

  // Now we try to open the file
  file_ = new QFile(fileName_);
  bool success = file_->open(IO_ReadOnly);

  if( ! success )
  {
    // Notify the user, even if debug mode is not enabled.
    kdWarning() << "PictureTransferP2P: Unable to open file: " << fileName_ << "!" << endl;

    // Close the file (also causes gotData() to fail)
    delete file_;
    file_ = 0;

#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
    kdDebug() << "PictureTransferP2P::contactStarted2_UserAccepts: Cancelling session" << endl;
#endif

    // Send 500 Internal Error back if we failed
    // the error will be ACK-ed.
    sendCancelMessage( CANCEL_FAILED );
    return;
  }


#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::contactStarted2_UserAccepts: Sending accept message" << endl;
#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.
 */
00328 void PictureTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage &/*message*/)
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - contactStarted3_ContactConfirmsAccept" << endl;
#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.
 */
00344 void PictureTransferP2P::contactStarted4_ContactConfirmsPreparation()
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - contactStarted4_ContactConfirmsPreparation" << endl;
#endif

  // Send the file, the base class handles everything else here
  sendData(file_, P2P_TYPE_PICTURE);
}



/**
 * Return the application's GUID.
 */
00359 QString PictureTransferP2P::getAppId()
{
  return "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}";
}



/**
 * Return the msn object of the picture we're transferring
 */
00369 const MsnObject & PictureTransferP2P::getMsnObject() const
{
  return msnObject_;
}



/**
 * Determinate the path for an contact picture.
 *
 * @param msnObject  The MSNObject the other contact uses to identify his resource.
 *
 * @returns  A file name string.
 */
00383 QString PictureTransferP2P::getPictureFileName(const MsnObject &msnObject)
{
  // Replace bad characters, in case someone intends to send a bad SHA1.
  // The sha1 string is actually base64 encoded, meaning we could
  // also expect a "/" character in the string.
  QString sha1d = msnObject.getDataHash();
  const QString safeSha1 = sha1d.replace(QRegExp("[^a-zA-Z0-9+=]"), "_");

  // Be friendly for file managers.
  QString extension;
  QString path;
  switch( msnObject.getType() )
  {
    case MsnObject::DISPLAYPIC:
      extension = ".png";
      path = "displaypics";
      break;

    case MsnObject::BACKGROUND:
      extension = ".png";
      path = "backgrounds";
      break;

    case MsnObject::EMOTICON:
      extension = ".png";
      path = "useremoticons";
      break;

    case MsnObject::WINK:
      extension = ".cab";
      path = "winks";
      break;

    case MsnObject::VOICECLIP:
      extension = ".wav";
      path = "voiceclips";
      break;

    default:
      extension = ".dat";
      path = QString::null;
  }

  // Locate filename
  return locateLocal( "data", QString("kmess/") + path + "/" + QString::fromUtf8( safeSha1 ) + extension);
}



/**
 * Called when data is received.
 * Writes the received data to the output file.
 *
 * @param  message  P2P message with the data.
 */
00438 void PictureTransferP2P::gotData(const P2PMessage &message)
{
  if(file_ == 0)
  {
    kdWarning() << "PictureTransferP2P::gotData() - Unable to handle file data: no file open or already closed "
                << "(offset="    << message.getDataOffset()
                << " totalsize=" << message.getTotalSize()
                << " contact="   << getContactHandle() << ")!" << endl;

    // 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 pictureReceived() 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.
 */
00477 bool PictureTransferP2P::isPrivateChatRequired() const
{
  // Picture 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)
 */
00495 void PictureTransferP2P::showEventMessage(const QString &message, const ChatMessage::ContentsClass contents, bool /*isIncoming*/ )
{
  // 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 picture transfer happen
  // in the background, it's not helping to have empty chat windows popping up with error messages.

  kdWarning() << "PictureTransferP2P::showEventMessage() - suppressed message: " << message << " (contact=" << getContactHandle() << " contentsClass=" << contents << ")" << endl;
}



/**
 * Called when the transfer is complete.
 * Closes the file, verifies the MsnObject and updates possible listeners.
 */
00510 void PictureTransferP2P::showTransferComplete()
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::showTransferComplete() - Last data part received, closing file" << endl;
#endif

  if( KMESS_NULL(file_) ) return;

  bool success = false;
  QString pictureFileName = file_->name();

  // 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.
  if( file_->open( IO_ReadOnly ) )
  {
    Sha1 sha1;
    QByteArray shaData( 20 ); // 160 bits of output
    shaData.duplicate( (const char *)sha1.calcSha1( (const byte *)file_->readAll().data(), file_->size() ), 20 );
    file_->close();
    QString hash = KCodecs::base64Encode( shaData );
    success = ( msnObject_.getDataHash() == hash );
  }

  // Clean up
  delete file_;
  file_ = 0;


  // Special checks for some types.
  switch( msnObject_.getType() )
  {
    case MsnObject::DISPLAYPIC:
    case MsnObject::BACKGROUND:
    case MsnObject::EMOTICON:
      // For pictures, see if it's broken.
      if( QPixmap::imageFormat( pictureFileName ) == 0 || ! success )
      {
        kdWarning() << "PictureTransferP2P::showTransferComplete() - Received image was broken (contact=" << getContactHandle() << ")." << endl;
        QFile::remove( pictureFileName );
        return;
      }

      break;

    // Avoid gcc warnings.
    default: break;
  }


  // Send an event to the switchboard:
  emit pictureReceived( 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.
 */
00577 void PictureTransferP2P::showTransferMessage( const QString &message )
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P::showTransferMessage() - suppressed message: " << message << endl;
#else
  Q_UNUSED( message ); // Avoid compiler warnings
#endif

  // WLM8 appears to initiate direct connections for picture 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
 */
00594 void PictureTransferP2P::userStarted1_UserInvitesContact()
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - userStarted1_UserInvitesContact - requesting display picture" << endl;
#endif

  // Set the filename
  fileName_ = getPictureFileName(msnObject_);

  // Encode the context
  // The \0 char is added on purpose, this fixes the picture 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().utf8();  // conversion from QCString to QByteArray, adding \0 char.
  context = QString::fromUtf8(KCodecs::base64Encode(rawObject));

  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;

    case MsnObject::VOICECLIP:
      // Unknown

    default:
      appId = 1;  // the old appId for msn object transfers
  }

  // Send the invitation
  sendSlpSessionInvitation( generateID(), getAppId(), appId, context);
}



/**
 * Step two of a user-started chat: the contact accepts
 *
 * @param  message  Accept message of the other contact
 */
00647 void PictureTransferP2P::userStarted2_ContactAccepts(const MimeMessage & /*message*/)
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - userStarted2_ContactAccepts" << endl;
#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.
 */
00664 void PictureTransferP2P::userStarted3_UserPrepares()
{
#ifdef KMESSDEBUG_PICTURETRANSFER_P2P
  kdDebug() << "PictureTransferP2P - userStarted3_UserPrepares" << endl;
#endif

#ifdef KMESSTEST
  ASSERT(   file_ == 0          );
  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
    return;
  }


  file_ = new QFile(fileName_);
  bool success = file_->open(IO_WriteOnly);

  if( ! success )
  {
    // Notify the user, even if debug mode is not enabled.
    kdWarning() << "PictureTransferP2P: Unable to open file: " << fileName_ << "!" << endl;

    // 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 "picturetransferp2p.moc"

Generated by  Doxygen 1.6.0   Back to index