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

p2papplication.cpp

/***************************************************************************
                          p2papplication.cpp -  description
                             -------------------
    begin                : Mon Nov 22 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 "p2papplication.h"

#include "../../kmessdebug.h"
#include "../../currentaccount.h"
#include "../../contact/msnobject.h"
#include "../../utils/kmessshared.h"
#include "../extra/msndirectconnection.h"
#include "../mimemessage.h"
#include "../p2pmessage.h"
#include "applicationlist.h"

#include <QRegExp>

#include <KLocale>


#ifdef KMESSDEBUG_P2PAPPLICATION
#define KMESSDEBUG_P2PAPPLICATION_GENERAL
#endif

// Settings for debugging.
#define P2PAPPLICATION_AVOID_DC_CLIENT 0
#define P2PAPPLICATION_AVOID_DC_SERVER 0

// Timeout settings, to prevent stale instances.
// Measured an ACK response time of 90 seconds once for an overloaded machine (Pentium 2 running Skype + WLM).
#define P2PAPPLICATION_TIMEOUT_ACCEPT   120000
#define P2PAPPLICATION_TIMEOUT_ACK      120000
#define P2PAPPLICATION_TIMEOUT_DATA     120000
#define P2PAPPLICATION_TIMEOUT_MOREDATA 120000
#define P2PAPPLICATION_TIMEOUT_SLP      120000  // 120 seconds
#define P2PAPPLICATION_TIMEOUT_DESTROY   10000  // 10 seconds



/**
 * @brief Constructor, initializes the P2PApplication instance fields.
 *
 * The ApplicationList object contains the direct connection, which is shared between all P2P applications of one same contact.
 * This object is also used to deliver messages back to the contact.
 *
 * @param  applicationList  The application list object for the contact
 */
00061 P2PApplication::P2PApplication(ApplicationList *applicationList)
: P2PApplicationBase( applicationList ),
  applicationList_( applicationList ),   // local copy for now, don't want to expose it as protected.
  gotSlpMessage_(false),
  invitationCSeq_(0),
  invitationSessionID_(0),
  sessionID_(0),
  userShouldAcknowledge_(false)
{
  // For debugging
  setObjectName( QLatin1String( metaObject()->className() ) + "[0/" + getContactHandle() + "]" );
}



/**
 * @brief Class destructor.
 *
 * Cleans up buffers and timers.
 */
00081 P2PApplication::~P2PApplication()
{
#ifdef KMESSDEBUG_APPLICATION
  kDebug() << " session=" << getSessionID();
#endif

  applicationList_ = 0;
}



/**
 * @brief The contact aborted the session
 *
 * This method is overwritten from the base class, so
 * endApplicationLater() is called instead of endApplication().
 * This leaves the P2P session open for a short while, to accept any remaining
 * packets that could be sent (like TCP's TIME_WAIT state).
 *
 * @param message Optional message to display, defaults to getContactAbortMessage().
 */
00102 void P2PApplication::contactAborted(const QString &message)
{
#ifdef KMESSDEBUG_APPLICATION
  kDebug() << "state=" << (int) getWaitingState();
#endif

  // Make sure a second message is not displayed here.
  // The contact could be sending multiple error messages.
  if( isWaitingState( P2P_WAIT_END_APPLICATION ) )
  {
#ifdef KMESSDEBUG_APPLICATION
    kDebug() << "Abort the application again, "
                "application was already awaiting termination.";
#endif

    // Make sure the timer is started again.
    endApplicationLater();
    return;
  }
  else if( isClosing() )
  {
    kWarning() << "Attempted to close application twice "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=endapplicationlater)";

    // Make sure the timer is started again.
    endApplicationLater();
    return;
  }


  if( isTransferComplete() )
  {
    // Make sure the user won't get a "transfer complete" message
    // followed by a "contact aborted" message.
    //
    // This could happen if:
    // - WLM sends data
    // - KMess acks
    // - WLM closes the SB
    // - contactAborted() called for every app.

    // Also interesting:
    // - signout / signin with WLM.
    // - establish a SB connection again by starting a chat.
    // - The P2P session is still active in WLM,
    //   and has a bridge now, so WLM sends the SLP BYE.
    // - kmess will ack it with a temporary p2p session.
    kWarning() << "not displaying message because data transfer is complete. "
                  "The switchboard was likely closed after receiving all data. "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=endapplicationlater)";
  }
  else
  {
    // Remove the accept links
    modifyOfferMessage();

    if( message.isEmpty() )
    {
      showEventMessage( getContactAbortMessage(), ChatMessage::CONTENT_APP_CANCELED, true );
    }
    else
    {
      showEventMessage( message, ChatMessage::CONTENT_APP_CANCELED, true );
    }
  }

  endApplicationLater();
}



/**
 * @brief Step 1 of a contact-started chat: the contact invites the user
 *
 * This default implementation automatically declines the invitation with a "feature not supported" notification.
 *
 * Override this method to handle the invitation instead.
 * It can typically do one of the following things:
 * - parse the 'Context' field of the invitation and store the information.
 * - display the accept/cancel link with offerAcceptOrReject().
 * - automatically accept the invitation by calling contactStarted2_UserAccepts().
 * - decline the invitation by calling sendCancelMessage().
 *
 * @param  message  The body (payload fields) of the SLP INVITE message,
 *                  which typically has the fields 'Context' and 'AppID'.
 *                  The 'SessionID' and 'EUF-GUID' fields are be processed automatically.
 */
00196 void P2PApplication::contactStarted1_ContactInvitesUser(const MimeMessage &message)
{
#ifdef KMESSDEBUG_APPLICATION
  kDebug() << "(rejects invitation)";
#endif

  // We can assume the switchboard already displayed a better error message
#ifdef KMESTEST
  kWarning() << "The Application subclass didn't implement contactStarted1_ContactInvitesUser!";
#endif

  // Send some debug info to the console
  MimeMessage slpMimeBody(message.getBody());
  kWarning() << "The contact sent an MSNSLP invitation type KMess can't handle yet "
                "(euf-guid=" << slpMimeBody.getValue("EUF-GUID") <<
                " appid="    << slpMimeBody.getValue("AppID")    <<
                " action=send500).";

  // Display a warning to the user
  showSystemMessage( i18n("The contact has invited you to an activity not yet supported by KMess."), ChatMessage::CONTENT_SYSTEM_NOTICE, true );


  // Cancel the invitation
  sendCancelMessage( CANCEL_NOT_INSTALLED );

  // Unlike Application::contactStarted1_ContactInvitesUser()
  // we don't quit here!! We should wait for the BYE message from the other contact!
}



/**
 * @brief Step 4 of a contact-started chat: the contact confirms the data-preparation message.
 *
 * This method was added for P2P-style chats to support the last step of a P2P session.
 * When the contact requests to send data, call sendData() here to start the transfer.
 */
00233 void P2PApplication::contactStarted4_ContactConfirmsPreparation()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug();
#endif
}



/**
 * @brief Return the identifier the SLP INVITE message.
 * @return The Branch-ID value from the SLP INVITE message.
 */
00246 QString P2PApplication::getBranch() const
{
  return branch_;
}



/**
 * @brief Return the identifier of the SLP session.
 * @return The Call-ID value from the SLP INVITE message.
 */
00257 QString P2PApplication::getCallID() const
{
  return callID_;
}



/**
 * @brief Return the Content-Type of the invitation message
 *
 * This method is useful for some contactStarted1_ContactInvitesUser() implementations.
 *
 * @return The Content-Type value, e.g. <tt>application/x-msnmsgr-sessionreqbody</tt>.
 */
00271 const QString& P2PApplication::getInvitationContentType() const
{
  return invitationContentType_;
}



/**
 * @brief Return the identifier of the SLP INVITE message.
 *
 * This method is useful for some contactStarted2_UserAccepts() implementations.
 * It's possible getSessionID() still returns zero, because the client should confirm the Session-ID first.
 *
 * @return The 'SessionID' field from the SLP payload of the invitation message.
 */
00286 quint32 P2PApplication::getInvitationSessionID() const
{
  return invitationSessionID_;
}



/**
 * @brief Return the session ID, this identifies the P2P session.
 *
 * It's the first field of the binary P2P header.
 * When a negotiation message is sent, it's session ID field will always be zero,
 * regardless of the real session ID.
 *
 * @return The session ID field used in the binary P2P header.
 */
00302 quint32 P2PApplication::getSessionID() const
{
  return sessionID_;
}



/**
 * @brief Parse the received ACK message.
 *
 * The messageType field is used to detect actions
 * like "contact ACKed the OK message" or "contact received all data".
 *
 * It handles the following ACK message types:
 * - ACK of a SLP INVITE message invokes gotAck_SlpSessionInvitation().
 * - ACK of a SLP session OK message invokes gotAck_slpSessionOk().
 * - ACK of a SLP tranfer INVITE message invokes gotAck_slpTransferInvitation().
 * - ACK of a SLP tranfer OK message invokes gotAck_slpTransferOk().
 * - ACK of a SLP transfer decline message invokes gotAck_slpTransferDecline().
 * - ACK of the data preparation message invokes gotAck_dataPreparation()
 * - ACK of the sent file data invokes gotAck_dataReceived().
 * - ACK of the SLP BYE message, invokes gotAck_slpBye().
 * - ACK of a SLP error message invokes gotAck_slpError().
 * - all other ACKs are not treated specially.
 *
 * @param  message           The ACK message.
 * @param  ackedMessageType  The type of the original message this ACK corresponds with.
 */
00330 void P2PApplication::gotAck(const P2PMessage &message, const P2PMessageType ackedMessageType)
{
#ifndef KMESSTEST
  Q_UNUSED( message ); // Avoid compiler warning
#endif

#ifdef KMESSTEST
  if( message.isAck() )  // normal 0x02 ack
  {
    KMESS_ASSERT( message.getDataSize()     == 0 );
    KMESS_ASSERT( message.getTotalSize()    == 0 );
    KMESS_ASSERT( message.getAckSessionID() != 0 );
    KMESS_ASSERT( message.getAckUniqueID()  != 0 );
    KMESS_ASSERT( message.getAckDataSize()  != 0 );
  }
#endif

  switch( ackedMessageType )
  {
    // Are we waiting for the contact to accept our invitation (e.g. send "200 OK")?
    case P2P_MSG_SESSION_INVITATION:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (INVITE ACK).";
#endif

      gotAck_slpSessionInvitation();
      return;


    // Are we waiting for the ACK to confirm our "200 OK" message?
    case P2P_MSG_SESSION_OK:

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (200 OK ACK).";
#endif

      gotAck_slpSessionOk();
      return;


    // Are we waiting for the contact to accept our transfer invitation (e.g. send "200 OK")?
    case P2P_MSG_TRANSFER_INVITATION:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (INVITE transfer ACK).";
#endif

      gotAck_slpTransferInvitation();
      return;


    // Are we waiting for the ACK to confirm our "200 OK" message?
    case P2P_MSG_TRANSFER_OK:

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (200 transfer OK ACK).";
#endif

      gotAck_slpTransferOk();
      return;


    // Are we waiting for the ACK to confirm our data preparation message?
    case P2P_MSG_DATA_PREPARATION:

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (data-preparation ACK).";
#endif
#ifdef KMESSTEST
      KMESS_ASSERT( message.getAckDataSize() == 4 );
#endif

      gotAck_dataPreparation();
      return;


    // Are we waiting for the ACK to confirm that all data was received?
    case P2P_MSG_DATA:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (all-received ACK).";
#endif

      gotAck_dataReceived();
      return;


    // Are we waiting for the BYE ACK to quit the session/application?
    case P2P_MSG_SESSION_BYE:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (BYE ACK, terminating).";
#endif

      gotAck_slpBye();
      return;


    // Are we expecting a ACK for our decline of the transfer?
    case P2P_MSG_TRANSFER_DECLINE:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (transfer decline ACK).";
#endif

      gotAck_slpTransferDecline();
      return;


    // Are we waiting for the SLP Error message to be ACK-ed?
    case P2P_MSG_SLP_ERROR:
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message (Confirmed SLP Error).";
#endif

      gotAck_slpError();
      return;


    // Standard ACK message or unhandled ACK message.
    default:

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Got an ACK message. "
                  "waitingState=" << (int) getWaitingState() << " ackedMessageType=" << ackedMessageType << ".";
#endif

      // See if the derived class needs to handle this ack.
      if( gotUnhandledAck( message, ackedMessageType ) )
      {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
        kDebug() << "Message is handled by gotUnhandledAck(), no further action needed.";
#endif
        return;
      }

      // Avoid warnings when we're waiting for something else, and the ACK is not important.
      if( isWaitingState( P2P_WAIT_FOR_CONNECTION ) )
      {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
        kDebug() << "No special action taken (waiting for a connection).";
#endif
      }
      else if( ackedMessageType != P2P_MSG_UNKNOWN || ! isWaitingState( P2P_WAIT_DEFAULT ) )
      {
        // Unexpected, this should be a normal ACK message.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
        kWarning() << "Unhandled state for ACK message:"
                      " waitingState=" << (int) getWaitingState() << " ackedMessageType=" << ackedMessageType << "!";
#endif
        // If we were waiting: the timer is stopped and
        // we forgot an if-block in this method.
        // So make sure the object will still be killed eventually.
        setWaitingState( getWaitingState(), P2PAPPLICATION_TIMEOUT_DATA );
        return;
      }
  }
}



/**
 * @brief Called when the ACK for the data preparation was received.
 *
 * This invokes initiateTransfer(), unless the the application
 * waits for an ACK to a SLP transfer OK message.
 */
00493 void P2PApplication::gotAck_dataPreparation()
{
#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) || isWaitingState( P2P_WAIT_FOR_CONNECTION ) );
#endif

  // Temporary switch state, if the derived class
  // sends a message this state will be changed.
  setWaitingState( P2P_WAIT_DEFAULT, 0 );

  // This should happen after the data preparation message is sent.

  // If the user sent the invitation,
  // we shouldn't receive the prepare-ack, but sent it.
  if(isUserStartedApp())
  {
    // We received a data preparation ACK but didn't expect it.
    kWarning() << "P2P message can't be handled, "
                  "this ack is not expected for applications started by the user "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x08).";

    // Send error back.
    sendP2PAbort();

    showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, false );
    endApplicationLater();
    return;
  }


  // Keep waiting if a direct connection setup is about to be complete.
  if( hasUnAckedMessage( P2P_MSG_TRANSFER_OK ) )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Application is waiting for a 200 transfer OK ACK, "
                "not initiating transfer yet.";
#endif

    setWaitingState( P2P_WAIT_FOR_TRANSFER_ACK, P2PAPPLICATION_TIMEOUT_SLP );
    return;
  }


  // Dispatch to derived implementation
  initiateTransfer();
}



/**
 * @brief Called when the ACK for the sent file data was received.
 *
 * This method invokes showTransferComplete(), and sends the SLP BYE if needed.
 */
00550 void P2PApplication::gotAck_dataReceived()
{
#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_DATA_ACK ) );
#endif

  if(isUserStartedApp())
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "All data sent and confirmed, "
                "sending BYE message...";
#endif

    // We sent the INVITE, now we send the BYE.
    sendSlpBye();

    // All data was acked
    // Telling derived class the transfer is complete.
    showTransferComplete();

    // Don't terminate yet, that message needs to be ACK-ed as well.
  }
  else
  {
#ifdef gotAck_dataReceived
    kDebug() << "All data sent and confirmed, "
                "waiting for BYE message...";
#endif

    // All data was acked, this is good enough.
    // The remaining teardown happens behind the scenes.
    showTransferComplete();

    // The other contact sent the INVITE, so we also wait for the BYE message.
    // This happens for msnobject transfers for example.
    setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP );
  }
}



/**
 * @brief Called when the ACK for the SLP BYE message was received.
 *
 * This schedules the application for termination.
 */
00596 void P2PApplication::gotAck_slpBye()
{
  if( ! isWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK ) )
  {
    kWarning() << "Unexpected SLP BYE received!";
  }

  // Test whether the data transfer to the contact was aborted.
  testDataSendingAborted();

  // Session complete.
  endApplicationLater();
}



/**
 * @brief Called when the ACK for a SLP Error was received.
 *
 * if the contact started the application, this will wait for the SLP BYE message.
 * Otherwise the application is terminated.
 */
00618 void P2PApplication::gotAck_slpError()
{
#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) );
#endif

  if(! isUserStartedApp())
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "The contact started this application, waiting for BYE.";
#endif

    // We wait for the BYE
    setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP );
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "The user started this application, terminating.";
#endif

    // TODO: send BYE after the client confirmed our SLP error?
    endApplicationLater();
  }
}



/**
 * @brief Called when the ACK for the first SLP INVITE message was received.
 *
 * This only changes the waiting state, to wait for the SLP OK message.
 */
00651 void P2PApplication::gotAck_slpSessionInvitation()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for 200 OK message...";
#endif

  // Continue to wait for the contact to send the accept message (SLP OK).
  setWaitingState( P2P_WAIT_CONTACT_ACCEPT, P2PAPPLICATION_TIMEOUT_ACCEPT );
  stopWaitingTimer();
}



/**
 * @brief Called when the ACK of the SLP OK message was received.
 *
 * This method invokes contactStarted3_ContactConfirmsAccept()
 */
00669 void P2PApplication::gotAck_slpSessionOk()
{
  // This should happen after the "200 OK" message is sent.

  // If the user started the inviation,
  // we send the SLP OK ack, not receive it
  if( isUserStartedApp() )
  {
    // We received an 200 OK ACK but didn't expect it.
    // This is very unlikely, but still prevent worse situations when contactStarted3_.. is invoked().
    kWarning() << "P2P message can't be handled, "
                  "this ack is not expected for applications started by the user "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x08).";

    // Show message
    showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, false );

    // Abort
    sendCancelMessage( CANCEL_ABORT );
    return;
  }

  // Reset, will be changed by initiateTransfer->contactStarted3..->sendDataPreparationAck.
  // TODO: rewrite the sending of the data-preparation message using the incoming/outgoing queue.
  setWaitingState( P2P_WAIT_DEFAULT, 0 );

  // SLP OK message ACKed
  // For P2P apps, we can't prove any mime fields here, so an empty constructor is what's left..
  // This is where the data preparation ACK could be sent.
  contactStarted3_ContactConfirmsAccept( MimeMessage() );

#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) || isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) );
#endif

  // If the contactStarted3() didn't sent anything, we'll wait for incoming file data.
  if( isWaitingState( P2P_WAIT_DEFAULT ) )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting state not changed, "
                "waiting for file data...";
#endif

    // For Pictures, the contact must send the data preparation ACK at this point (P2P_WAIT_FOR_PREPARE_ACK)
    // For Files, the contact must initiate the data transfer now. (P2P_WAIT_DEFAULT)
    setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA );
  }
}



/**
 * @brief Called when the ACK for the SLP transfer decline message was received.
 *
 * This only changes the waiting state,
 * the contact should start to send the data now.
 */
00729 void P2PApplication::gotAck_slpTransferDecline()
{
#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) || isWaitingState( P2P_WAIT_FOR_FILE_DATA ) );
#endif

  if( isTransferActive() || isWaitingState( P2P_WAIT_FOR_FILE_DATA ) )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Got ack while data transfer is already active.";
#endif
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for file data...";
#endif

    // Continue to wait for the contact to send the file data
    setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA );
  }
}



/**
 * @brief Called when the ACK for the SLP transfer INVITE message was received.
 *
 * This only changes the waiting state, to wait for the SLP OK message.
 */
00759 void P2PApplication::gotAck_slpTransferInvitation()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for 200 OK message...";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_INVITE_TR_ACK ) );
#endif

  // Continue to wait for the contact to send the accept message (SLP OK).
  setWaitingState( P2P_WAIT_FOR_TRANSFER_ACCEPT, P2PAPPLICATION_TIMEOUT_ACCEPT );

  // note KMess uses a fixed time user to accept/decline here.
  // WLM8 has no timeout here for file transfer.
}



/**
 * @brief Called when the ACK for the SLP transfer OK mesages was received.
 *
 * This invokes initiateTransfer() or waits
 * for an other direct connection to complete.
 */
00783 void P2PApplication::gotAck_slpTransferOk()
{
  // For some reason, our "MSNSLP/1.0 200 OK" message can be ACKed when we just ACKed all data is received.
  // Detect this, otherwise the chat window is filled with "File transfer complete.. Contact aborted" messages.
  if( isTransferComplete() || isWaitingState( P2P_WAIT_FOR_SLP_BYE ) )
  {
    kWarning() << "Received ACK for 'transrespbody' message "
                  "while closing the session "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=return).";

    // Make sure the timer starts again.
    setWaitingState( getWaitingState(), P2PAPPLICATION_TIMEOUT_SLP );
    return;
  }

  // Temporary switch the waiting state.
  setWaitingState( P2P_WAIT_DEFAULT, 0 );

  // This should happen after the "200 OK" message is sent.

  // SLP ransfer OK message ACKed
  if( ! applicationList_->hasDirectConnection()
  &&  ! applicationList_->hasPendingConnections() )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "No connection attempts were made, "
                "so starting transfer immediately.";
#endif
    // TODO: the contact may actually send an INVITE back to become the server.
    initiateTransfer();
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "200 transfer OK ACK received, "
                "but waiting for connection attempts to succeed or fail...";
#endif
  }

  // If the contactStarted3() didn't sent anything, we'll wait for incoming file data.
  if( isWaitingState( P2P_WAIT_DEFAULT ) )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting state not changed, "
                "waiting for file data...";
#endif

    // At some slow systems, it seams WLM can already start
    // the data transfer before waiting for a connection.
    if( isTransferActive() )
    {
      kWarning() << "already received " << getTransferredBytes() << " bytes before receiving the 200 transfer OK ACK "
                    "(state="   << (int) getWaitingState() <<
                    " dc="      << applicationList_->hasDirectConnection() <<
                    " dcauth="  << applicationList_->hasAuthorizedDirectConnection() <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=continue).";
    }

    // For Files, the contact must initiate the data transfer now. (P2P_WAIT_DEFAULT)
    setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA );
  }
}



/**
 * @brief Called when data is received.
 *
 * This method should be overwritten in the derived class
 * to store the data in a file, buffer, etc..
 *
 * The order of the data fragments is not guaranteed by WLM, especially when it's switching to a direct connection.
 * The writeP2PDataToFile() function can be used to cope with this behavour.
 * Also don't rely on P2PMessage::isLastFragment() to determine when the transfer is complete.
 * Overwrite showTransferComplete() instead.
 *
 * @param  message  The P2P message containing the data.
 */
00868 void P2PApplication::gotData(const P2PMessage &/*message*/)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kWarning() << "Data not handeled by derived class.";
#endif
}



/**
 * @brief Called internally after all data is received.
 *
 * It determines how to continue the session.
 *
 * It's possible to overwrite this method.
 * However don't forget to class this parent method too
 * if you want to make sure the BYE message is sent/received properly.
 */
00886 void P2PApplication::gotDataComplete( const P2PMessage &lastMessage )
{
  Q_UNUSED( lastMessage );

  // Determine what to send next
  if(isUserStartedApp())
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Application was started by the user, also send SLP BYE.";
#endif

    // Current state could be P2P_WAIT_FOR_FILE_DATA, but reset it here.
    // Make the debugging clean.
    setWaitingState( P2P_WAIT_DEFAULT, 0 );

    // We sent the INVITE, now we send the BYE.
    sendSlpBye();
    // Don't terminate yet, that message needs to be ACK-ed as well.
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Application was started by the contact, waiting for SLP BYE...";
#endif

    // Otherwise we wait for the BYE
    setWaitingState( P2P_WAIT_FOR_SLP_BYE, P2PAPPLICATION_TIMEOUT_SLP );
  }
}



/**
 * @brief Got an direct connection handshake ack.
 *
 * This method verifies the Nonce field and marks the connection as authenticated.
 * If the Nonce field is invalid, the connection will be closed without any notice.
 *
 * @param  message  The handshake message received from the direct connection.
 */
00926 void P2PApplication::gotDirectConnectionHandshake(const P2PMessage &message)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got a direct connection handshake message.";
#endif

  // If the contact was sending a handshake when we dropped the connection,
  // it's possible this packet will be delivered over the switchboard.
  if( ! applicationList_->hasDirectConnection() )
  {
    // Ignore the packet
    kWarning() << "Received direct connection handshake at the switchboard "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=return).";
    return;
  }

  MsnDirectConnection *directConnection = applicationList_->getDirectConnection();

  // Get connection info
  QString nonce    ( message.getNonce() );
  bool    isServer = directConnection->isServer();

  // Verify the nonce
  // Nonce is already used in ApplicationList to find this application,
  // but this is kept here for consistency, and if ApplicationList would change one time.
  if( nonce != nonce_ )
  {
    // Invalid nonce! Drop connection
    kWarning() << "Contact sent invalid nonce "
                  "(received=" << nonce <<
                  " expected=" << nonce_ <<
                  " contact="  << getContactHandle() <<
                  " session="  << sessionID_ <<
                  " class="    << metaObject()->className() <<
                  " action=closeconnection).";

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Closing connection, moving transfer to switchboard.";
#endif

    // Like the official client, close the connection without any notice.
    // The transfer continues over the switchboard instead.
    directConnection->closeConnection();    // DirectConnectionPool deletes it.
  }
  else if( isServer )
  {
    // If we're the server, we still need to send a handshake back
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Contact sent correct nonce, "
                "sending direct connection handshake ACK.";
#endif
#ifdef KMESSTEST
    KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_HANDSHAKE ) || isTransferActive() );
#endif

    // KMess is the server, it should authenticate the handshake message.
    sendDirectConnectionHandshake( nonce_ );
  }
  else
  {
    // KMess is the client, it should verify the handshake response only.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Contact sent correct nonce, "
                "marking connection as authorized.";
#endif

#ifdef KMESSTEST
    KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_HANDSHAKE_OK ) || isTransferActive() );
#endif

    // Detect when the contact sent a second INVITE, runs the direct connection server, but sends the nonce first.
    // This happened once with WLM8 after the data transfer because KMess didn't send the handshake.
    if( isWaitingState( P2P_WAIT_FOR_HANDSHAKE ) )
    {
      kWarning() << "received nonce from contact first, "
                    "but it's running the server"
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=send0x100)!";

      sendDirectConnectionHandshake( nonce_ );
    }
  }


  // See if the transfer was already started.
  if( isTransferComplete() )
  {
    kWarning() << "connection is authorized "
                  "but data transfer is already complete"
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x100)!";
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "marking connection as authorized, transfer should start.";
#endif

    // Note this could be a reverse invitation.

    setWaitingState( P2P_WAIT_FOR_FILE_DATA, 0 );  // no timeout, happens by DirectConnectionPool.
  }

  // Mark connection as authorized
  // This also starts signals which call slotConnectionAuthorized()
  directConnection->setAuthorized(true);
}



/**
 * @brief Called when the data preparation message is received.
 *
 * It calls initiateTransfer() to inform the derived class.
 * When sendDataPreparationAck() is not called by the derived class,
 * the session will abort automatically.
 *
 * @param  message  The P2P data preparation message.
 */
01053 void P2PApplication::gotDataPreparation(const P2PMessage &/*message*/)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got the data preparation message";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( ! isP2PAckSent() );
  // WLM invites for a DC first before sending the data preparation.
  // Since KMess declines this with a 603, P2P_WAIT_FOR_SLP_ERR_ACK is also a valid state here.
  KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_PREPARE ) || isWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK ) );
#endif


  // Tell the derived class it can send the data.
  // You typically want to call sendDataPreparation() or sendCancelMessage() here.
  // sendDataPreparationAck() only sets a boolean, so we're able to send the
  // correct ack or error message here. The methods changes one the following variable:
  userShouldAcknowledge_ = true;

  // Let the derived class call the methods.
  initiateTransfer();

  // if sendDataPreparation() was not called, determine now what to send:
  if( userShouldAcknowledge_ )
  {
    kDebug() << "Handling of data preparation failed "
                "(contact=" << getContactHandle() << " action=send0x08)";

    // Derived class still didn't acknowledge or cancel at all
    // (both disable userShouldAcknowledge_). This shouldn't occur!

    // Reject the data-preparation message
    showEventMessage( i18n("The transfer failed. Data preparation failed."), ChatMessage::CONTENT_APP_FAILED, false );

    // Initiate abort.
    sendP2PAbort();
    return;
  }


  // If the user did acknowledge something, we no longer have to send an ack any more
  // it must have been sendCancelMessage(CANCEL_ABORT) calling sendP2PAbort().
  if( isP2PAckSent() )
  {
    // No error message here, assume your derived class already displayed something.
    // If sendP2PAck() was called, the app won't terminate, so end it now.
    endApplicationLater();
    return;
  }


  // TODO: for WLM8, we need to delay the P2P ack message until the DC is established.

  // User did acknowledge, we can send the *actual* preparation ACK now.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Data preparation successful, sending ACK";
#endif

  sendP2PAck();


  // Now wait for the data to arrive
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for file data to arrive...";
#endif

  setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA );
}



/**
 * @brief Called when an complete SLP message is received.
 *
 * These messages are used to negotiate session parameters.
 * The SLP message itself also has a MIME header and MIME body.
 *
 * This method validates the 'To' field,
 * and dispatches the to the correct handler:
 * - messages starting with <tt>INVITE MSNMSGR</tt> are delivered to gotSlpInvite().
 * - messages starting with <tt>MSNSLP/</tt> are delivered to gotSlpStatus().
 * - messages starting with <tt>BYE MSNMSGR</tt> are delivered to gotSlpBye().
 * - other messages are rejected with an P2P error.
 *
 * The P2P message is not ACKed yet, each handler does this
 * when it's able to parse the SLP body.
 *
 * A preamble could look like one of the following:
 * @code
INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
@endcode
@code
MSNSLP/1.0 200 OK
@endcode
@code
BYE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
@endcode
 *
 * The message itself looks like:
 * @code
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
Via: MSNSLP/1.0/TLP ;branch={068676F2-9B0C-4945-8ABA-4E6F585F710F}
CSeq: 0 
Call-ID: {E9BCFBDB-2768-4BD3-9E4C-A97DC70261E8}
Max-Forwards: 0
Content-Type: application/x-msnmsgr-sessionreqbody
Content-Length: 348

{the message contents}
@endcode
 *
 * @param  slpMessage  The payload extracted from the P2P message fragments (the MIME fields and body).
 * @param  preamble    The first status line sent before the MIME fields, which describes the message type.
 */
01168 void P2PApplication::gotNegotiationMessage(const MimeMessage &slpMessage, const QString &preamble)
{
  // Parse the "negotiation message" (P2PMessage with sessionID 0)
  // Verify it is indeed directed to us..
  QString msgTo( slpMessage.getValue("To") );

  if(! msgTo.isEmpty())
  {
    // Only test if the field is available. If it's not, either something
    // is wrong in this application, or the client sent a bad message and
    // we should respond with an 500 error instead.
    // For some reason this can be received in upper case too if the user entered it that way (likely happens with 3rd party clients).
    if( msgTo.toLower() != "<msnmsgr:" + CurrentAccount::instance()->getHandle().toLower() + ">" )
    {
      // In the future, the <msnmsgr: > string might be replaced with
      // something to support MSN to AOL chats, etc..
      kWarning() << "P2P message can't be handled, "
                    "addressed to someone else "
                    "(to="       << msgTo <<
                    " contact="  << getContactHandle() <<
                    " session="  << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=send404).";

      showEventMessage( i18n("The invitation was cancelled. Message was not directed to us."), ChatMessage::CONTENT_APP_CANCELED, true );

      // Send error message back
      sendSlpError("404 Not Found"); // Waits for ACK and terminates
      return;
    }
  }

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got an SLP message, preamble=" << preamble << ".";
#endif



  // Find out what kind of message this is:
  if( preamble.startsWith("INVITE MSNMSGR") )
  {
    // Got an invitation!
    gotSlpInvite( slpMessage );
  }
  else if( preamble.startsWith("MSNSLP/") )
  {
    // Error message, or "MSNSLP/1.0 200 OK" message
    gotSlpStatus( slpMessage, preamble );
  }
  else if( preamble.startsWith("ACK MSNMSGR") )
  {
    // Got a SLP ACK message (Windows Live Messenger feature)
    gotSlpAck( slpMessage );
  }
  else if( preamble.startsWith("BYE MSNMSGR") )
  {
    // Got a BYE message
    gotSlpBye( slpMessage );
  }
  else
  {
    // Unknown SLP message
    kWarning() << "P2P message can't be handled, "
                  "unsuppored SLP negotiation message "
                  "(preamble=" << preamble <<
                  " contact="  << getContactHandle() <<
                  " session="  << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x08).";

    // Got an unknown SLP preamble
    sendP2PAbort();

    // Abort the application
    showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true );
    return;
  }
}



/**
 * @brief Called when an SLP ACK message was received.
 *
 * This message should not be confused with a P2P ACK.
 *
 * Windows Live Messenger uses this message to acknowlegde
 * IP/port information for a direct connection.
 *
 * This method is currently a STUB to keep Windows Live Messenger happy.
 *
 * One of the observed messages is:
 * @code
ACK MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
Via: MSNSLP/1.0/TLP ;branch={67683AAA-A6F2-4B11-895F-EF46B483F3A9}
CSeq: 0
Call-ID: {00000000-0000-0000-0000-000000000000}
Max-Forwards: 0
Content-Type: application/x-msnmsgr-transudpswitch
Content-Length: 157

IPv4ExternalAddrsAndPorts: 111.222.333.444:3495
IPv4InternalAddrsAndPorts: 192.168.1.12:3495
SessionID: 15076776
SChannelState: 0
Capabilities-Flags: 1
@endcode
 *
 * @code
ACK MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
Via: MSNSLP/1.0/TLP ;branch={BBE6AAC0-10F4-4419-B01C-1F7C2838BC3D}
CSeq: 0
Call-ID: {00000000-0000-0000-0000-000000000000}
Max-Forwards: 0
Content-Type: application/x-msnmsgr-transdestaddrupdate
Content-Length: 197\r\n
\r\n
IPv4ExternalAddrsAndPorts: 11.222.3.44:62570\r\n
IPv4InternalAddrsAndPorts: 192.168.1.69:1343\r\n
IPv4External-Connecting-Port-End-Range: 62576\r\n
SessionID: 15751111\r\n
SChannelState: 0
Capabilities-Flags: 1
@endcode
 *
 * @param  slpMessage  The parsed SLP message.
 */
01299 void P2PApplication::gotSlpAck(const MimeMessage &slpMessage )
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got SLP ACK message, usage is unknown. Message dump follows.\n"
            << slpMessage.getFields() << "\n"
            << slpMessage.getBody();
#else
  Q_UNUSED( slpMessage );
#endif

  bool emptySession = ( ! isFirstMessageSent() || getMode() == APP_MODE_ERROR_HANDLER );

  // This is some internal message of WLM8.
  // Accept it for now.
  sendP2PAck();

  // Also terminate this instance if it was just created for this SLP ACK messasge.
  if( emptySession )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Directly terminating temporary P2PApplication instance for SLP ACK message.";
#endif
    endApplication();
  }
}



/**
 * @brief Called when an SLP BYE message was received.
 *
 * It sends the ACK back, and calls endApplication() to terminate the session.
 * A contact may sent a BYE message to abort the transfer.
 * In this case, contactAborted() will be called instead.
 *
 * The SLP BYE message looks like:
 * @code
BYE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
Via: MSNSLP/1.0/TLP ;branch={A1DBCADF-6DA2-43FE-BE27-0AEE26816D91}
CSeq: 0
Call-ID: {E9BCFBDB-2768-4BD3-9E4C-A97DC70261E8}
Max-Forwards: 0
Content-Type: application/x-msnmsgr-sessionclosebody
Content-Length: 40

SessionID: 114164
SChannelState: 0
@endcode
 *
 * @param  slpMessage  The parsed SLP message.
 */
01352 void P2PApplication::gotSlpBye( const MimeMessage &slpMessage )
{
  Q_UNUSED( slpMessage ); // Avoid compiler warning

  // If the BYE was received early, it means the session was aborted prematurely.
  //
  // There appears to be a small bug in MSN 7.0. It doesn't sent the final "all data received" ACK,
  // but sends the BYE directly. After we ACK it, we get a "BYE sent" message back.
  // This issue appears to be fixed in MSN 7.5.
  //
  // Microsoft Messenger for the Mac (6.0.3) takes this even further with msnobject transfers.
  // It sends the BYE first and the "all data received" ACK afterwards.
  // This happens when KMess is already in the P2P_WAIT_END_APPLICATION state (see "action=unsetclosing").

  // Also terminate without errors if we're waiting for a BYE ACK and instead receive another BYE. This
  // means that both clients have finished sending, so don't issue errors. This fixes the double message
  // "transfer completed" followed by "transfer canceled" when sending files to Mercury Messenger.

  setCurrentMessageType( P2P_MSG_SESSION_BYE );
  sendP2PAck();

  bool isComplete = isTransferComplete();  // TODO: this only works for incoming data.

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got SLP BYE message, closing application "
              "(state=" << (int) getWaitingState() << ").";
#endif

  // Make sure the data is not sent anymore
  abortDataSending();


  if( ! isWaitingState( P2P_WAIT_FOR_SLP_BYE )
  &&  ! isWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK )
  &&  ! isWaitingState( P2P_WAIT_FOR_DATA_ACK ) )   // HACK: added for Messenger for the Mac 6.0.3 and MSN 7.0
  {
    // Make a final check if our states have been messed up. Had the following situation once:
    // - WLM invites to send a file
    // - KMess hosted the server socket.
    // - WLM started to send data.
    // - transfer was complete, acked, waiting for BYE
    // - WLM establishes the connection with the socket.
    // - connection is authenticated.
    // - sockets are disconnected
    // - WLM sends BYE
    if( isComplete )
    {
      kWarning() << "Transfer seems complete but state suggests otherwise "
                    "(state="   << (int) getWaitingState() <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=continue)";
      endApplicationLater();
    }
    else if( isWaitingState( P2P_WAIT_END_APPLICATION ) )
    {
      // When WLM aborts at a very late stage, it actually processes our final data messages,
      // ack's those, handle our BYE, and send it's "aborting BYE" afterwards.
      kWarning() << "Received BYE to abort when all data is already transferred "
                    "(state="   << (int) getWaitingState() <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplicationlater)";

      // Resume timer
      endApplicationLater();
    }

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "BYE message was unexpected; contact aborted "
              << "(state=" << (int) getWaitingState() << " contact=" << getContactHandle() << ")";
#endif

    // End with failure, bye sent at a different moment.
    contactAborted();   // closes the application

    // TODO: after this, the contact should send a 0x40 or 0x80 ack of the data stream.
    //       the message flag indicates whether the contact aborted it's own stream, or aborted ours.
  }
  else
  {
    // Normal end
    endApplicationLater();
  }
}



/**
 * @brief Called when a SLP INVITE message was received.
 *
 * It extracts the header fields, updates the instance fields, and parses the MIME body.
 * The invitation message is delivered to one of the following methods:
 * - The <tt>application/x-msnmsgr-sessionreqbody</tt> message is delivered to gotSlpSessionInvitation().
 * - The <tt>application/x-msnmsgr-transreqbody</tt> message is delivered to gotSlpTransferInvitation().
 * - The <tt>application/x-msnmsgr-transrespbody</tt> message is delivered to gotSlpTransferResponse(),
 *   with the <tt>secondInvite</tt> parameter set to <tt>true</tt>.
 * - Other messages are rejected as "unrecognized content-type".
 *
 * @param  slpMessage  The parsed SLP message.
 */
01455 void P2PApplication::gotSlpInvite(const MimeMessage &slpMessage)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got SLP INVITE message";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT(  isWaitingState( P2P_WAIT_DEFAULT )         // First invitation
              || isWaitingState( P2P_WAIT_FOR_FILE_DATA )   // Second invitation for file transfers.
              || isWaitingState( P2P_WAIT_FOR_PREPARE )     // WLM8: reverse invitation for msnobject transfer, holds back it's data-preparation message.
              || isWaitingState( P2P_WAIT_FOR_PREPARE_ACK ) // WLM8: second invitation for msnobject transfer, we already sent data-preparation ack.
              );
#endif

  // Reset the waiting state if this is the second INVITE for a file transfer
  // TODO: test what to do for the new WLM8 feature (sending transfer invites before data preparation).
  if( isWaitingState( P2P_WAIT_FOR_FILE_DATA ) )
  {
    setWaitingState( P2P_WAIT_DEFAULT, 0 );
  }

  // Indicate this is an SLP message (sendCancelMessage() uses this)
  // This value is reset once a message is sent.
  gotSlpMessage_ = true;


  // Session invitation
  // Extract the fields of the message. This is required for userRejected()
  MimeMessage slpMimeBody( slpMessage.getBody() );

  // Set global fields from INVITE message.
  QString slpVia         ( slpMessage.getValue("Via") );
  callID_                = slpMessage.getValue("Call-ID");
  invitationCSeq_        = slpMessage.getValue("CSeq").toInt();
  invitationContentType_ = slpMessage.getValue("Content-Type");

  // Extract branch from the "Via" parameter
  QRegExp callRE(";branch=(.+)");     // don't use a guid-pattern here, msn6 seams to accept random strings.
  callRE.indexIn( slpVia );
  branch_ = callRE.cap(1);


  // Don't forget the initialize the base class
  // The cookie is empty if this is a contact-started session.
  if( getCookie().isEmpty() )
  {
    startByInvite(generateCookie());
  }


  // Handle invitation based on the Content-Type field
  if( invitationContentType_ == "application/x-msnmsgr-sessionreqbody" )
  {
    // Send the ACK message
    setCurrentMessageType( P2P_MSG_SESSION_INVITATION );
    sendP2PAck();

    // Client invites us for a session
    // Will set the waitingState to P2P_WAIT_USER_ACCEPT later.
    gotSlpSessionInvitation( slpMimeBody );
  }
  else if( invitationContentType_ == "application/x-msnmsgr-transreqbody" )
  {
    // Send the ACK message
    setCurrentMessageType( P2P_MSG_TRANSFER_INVITATION );
    sendP2PAck();

    // Client requested to transfer the session
    // Handle the invitation internally
    gotSlpTransferInvitation( slpMimeBody );
  }
  else if( invitationContentType_ == "application/x-msnmsgr-transrespbody" )
  {
    // Send the ACK message
    setCurrentMessageType( P2P_MSG_TRANSFER_INVITATION );
    sendP2PAck();

    // Client sent a second invitation to become a direct-connection server because we can't be.
    gotSlpTransferResponse( slpMimeBody, true );
  }
  else
  {
    // Send the ACK message
    sendP2PAck();

    // Client sent an unknown invitation type
    kWarning() << "Received unexpected Content-Type "
                  "(type="    << invitationContentType_ <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send500).";

    // Indicate we don't like that content-type:
    sendCancelMessage(CANCEL_INVALID_SLP_CONTENT_TYPE);
    // Don't QUIT, the error will be ACK-ed.
  }

  // Reset state again.
  // So if the base class calls sendCancelMessage() outside
  // this method, it won't respond with a SLP message.
  gotSlpMessage_ = false;
}



/**
 * @brief Called when a SLP 200 OK message was received.
 *
 * This method is invoked from gotSlpStatus().
 * It extracts the Content-Type status code, depending on the value it proceeds with a different action:
 * - The payload of the <tt>application/x-msnmsgr-sessionreqbody</tt> message
 *   is delivered to userStarted2_ContactAccepts().
 * - The payload of the <tt>application/x-msnmsgr-transrespbody</tt> message
 *   is delivered to gotSlpTransferResponse().
 * - Any other message is rejected with a SLP error.
 *
 * A session confirmation looks like:
 * @code
MSNSLP/1.0 200 OK
To: <msnmsgr:contact@kmessdemo.org>
From: <msnmsgr:user@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-sessionreqbody
Content-Length: 22

SessionID: 114164
@endcode
 *
 * For an example of a transfer confirmation, see gotSlpTransferResponse().
 *
 * @param  slpMessage  The parsed SLP message.
 */
01587 void P2PApplication::gotSlpOk(const MimeMessage &slpMessage)
{
  // With a 200 code, we reset the waiting state for every situation,
  // to make the next check work.
  setWaitingState( P2P_WAIT_DEFAULT, 0 );

  // Parse the data of the message
  QString     contentType( slpMessage.getValue("Content-Type") );
  MimeMessage slpMimeBody( slpMessage.getBody() );  // body consists of more mime fields


  // The content-type tells us which fields are present
  if( contentType == "application/x-msnmsgr-sessionreqbody" )
  {
    // Message confirms the SessionID
#ifdef KMESSTEST
    KMESS_ASSERT( slpMimeBody.getValue("SessionID").toULong() == getSessionID() );
#endif

    // Send ACK now, with proper message type.
    setCurrentMessageType( P2P_MSG_SESSION_OK );
    sendP2PAck();

    // Tell the derived class the invitation was accepted
    userStarted2_ContactAccepts(slpMimeBody);
  }
  else if( (contentType == "application/x-msnmsgr-transrespbody") ||
            // HACK: allow transreqbody too for broken clients, including KMess 1.4.2:
           (contentType == "application/x-msnmsgr-transreqbody" && slpMimeBody.hasField("Nonce") ))
  {
    // Send ACK now, with proper message type.
    setCurrentMessageType( P2P_MSG_TRANSFER_OK );
    sendP2PAck();

    // Message tells us how we can create the direct connection.
    // This response is handled here, so it's transparent for the derived class.
    gotSlpTransferResponse( slpMimeBody );
  }
  else
  {
    sendP2PAck();

    // Unsupported response type.
    kWarning() << "Received unexpected 200/OK message "
                  "(type="    << contentType <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send500)!";

    showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true );

    // Send error message back
    sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE );
    return;
  }


  // Determine whether we should be waiting for the data-preparation message.
  // If we didn't send anything that changed the state and no transfer has started,
  // we're likely waiting for some kind of prepare message
  if( isWaitingState( P2P_WAIT_DEFAULT ) && ! isTransferActive() )
  {
    // Waiting for prepare message
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for contact to send some prepare message...";
#endif

    setWaitingState( P2P_WAIT_FOR_PREPARE, P2PAPPLICATION_TIMEOUT_ACK );
  }
}



/**
 * @brief Called when a SLP session invitation was received.
 *
 * This method stores the session ID and
 * invokes contactStarted1_ContactInvitesUser().
 * That method should parse the 'Context' field to handle the invitation.
 *
 * An SLP INVITE message for a msnobject transfer looks like:
 * @code
INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-sessionreqbody
Content-Length: 348

EUF-GUID: {A4268EEC-FEC5-49E5-95C3-F126696BDBF6}
SessionID: 114164
AppID: 12
Context: PG1zbm9ia..
@endcode
 *
 * @param  slpMimeBody  The SLP message with the fields 'EUF-GUID', 'SessionID', 'AppID' and 'Context'.
 */
01685 void P2PApplication::gotSlpSessionInvitation( const MimeMessage &slpMimeBody )
{
  // Read requested session id from the message
  invitationSessionID_ = slpMimeBody.getValue("SessionID").toUInt();

  // Don't accept invites if we sent one.
  if( ! isFirstMessageSent() && isUserStartedApp() )
  {
    kWarning() << "Got an INVITE response for "
                  "user-started application, rejecting "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send500).";

    sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE );
    return;
  }

  // For debugging
  setObjectName( QLatin1String( metaObject()->className() ) +
                 "[" + QString::number( invitationSessionID_ ) + "/" + getContactHandle() + "]" );

  // Mark waiting state for user.
  // Don't set a timeout just as WLM.
  setWaitingState( P2P_WAIT_USER_ACCEPT, 0 );   // state is used later to abort with 500 instead of BYE.

  // Tell the derived class we've got an invitation
  contactStarted1_ContactInvitesUser( slpMimeBody );
}



/**
 * @brief Called when a SLP status message was received.
 *
 * This method extracts the status code, depending on the status code it proceeds with a different action:
 * - The <tt>200 OK</tt> message invokes gotSlpOk().
 * - The <tt>404 Not Found</tt> message invokes contactAborted().
 * - THe <tt>481 No Such Call</tt> message invotes contactAborted().
 * - The <tt>500 Internal Error</tt> message invokes contactAborted().
 * - The <tt>603 Decline</tt> message invokes contactRejected().
 * - Any other message is rejected with a P2P error.
 *
 * @param  slpMessage  The parsed SLP message.
 * @param  preamble    The status line sent before the MIME fields,
 *                     which contains the error code (much like HTTP status codes).
 */
01733 void P2PApplication::gotSlpStatus(const MimeMessage &slpMessage, const QString &preamble)
{
  // An Error message, or "MSNSLP/1.0 200 OK" message
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Got SLP status message (" << preamble << ")";
#endif

  // See if this object was initialized to handle a Bad packet.
  if(getMode() == APP_MODE_ERROR_HANDLER)
  {
    // This object was initialized to send an error response for an unknown packet (at SLP level).
    showSystemMessage( i18n("The transfer failed. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_SYSTEM_NOTICE, true );
    sendSlpError("500 Internal Error"); // Waits for ACK and terminates
    return;
  }

  // Should only be received if we started it.
  if(! isUserStartedApp())
  {
    kWarning() << "P2P message can't be handled, "
                  "unexpected SLP response containing status code "
                  "(preamble=" << preamble <<
                  " contact="  << getContactHandle() <<
                  " session="  << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x08).";

    sendP2PAbort();

    // Abort the application
    showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true );
    return;
  }


  // Parse the preamble
  QRegExp re("MSNSLP/\\d+\\.\\d+ (\\d+) ([^\r\n]+)");
  re.indexIn( preamble );
  int     code  = re.cap(1).toUInt();
  QString detail( re.cap(2) );


  // Reset the waiting state if we were waiting.
  if( isWaitingState( P2P_WAIT_CONTACT_ACCEPT ) )
  {
    setWaitingState( P2P_WAIT_DEFAULT, 0 );
  }

  // Handle the status codes
  switch( code )
  {
    // If the invitation was accepted
    case 200:
      // We don't need to do much here:
      // The message will be ACK-ed with the correct session ID automatically
      // because that ID was given in the sendSlpInvitation() method.
      // That will be the first ACK to be sent with the SessionID set.
      
      // NOTE: sendAck() is not called here, but in gotSlpOk() so
      // it can set setCurrentMessageType() before the sendAck() call.

      // Parse in separate function
      gotSlpOk( slpMessage );
      break;


    // We sent a bad address in the to: header
    case 404:
      setCurrentMessageType( P2P_MSG_SLP_ERROR );
      sendP2PAck();

      // Inform the user KMess has an error
      kWarning() << "KMess sent a bad address in the 'to:' header "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=contactaborted).";
      contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") );
      break;


    // Somehow a message was sent twice (e.g. BYE)
    // or the contact aborts exactly when all data is being received + acked (so we sent the BYE).
    case 481:  // No Such Call
      setCurrentMessageType( P2P_MSG_SLP_ERROR );
      sendP2PAck();

      // Content-Type of this message is "application/x-msnmsgr-session-failure-respbody"

      // Inform the user KMess has an error
      kWarning() << "KMess sent an invalid Call-ID "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=contactaborted).";
      contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") );
      break;


    // Something was not supported
    case 500:
      setCurrentMessageType( P2P_MSG_SLP_ERROR );
      sendP2PAck();

      // Content-Type of this message is application/x-msnmsgr-session-failure-respbody

      if( slpMessage.getValue("Content-Type") == "null" )
      {
        // The Content-Type was not supported
        kWarning() << "Content-Type of our invitation was not understood "
                      "(type=" << invitationContentType_ <<
                      " contact=" << getContactHandle() <<
                      " session=" << sessionID_ <<
                      " class="   << metaObject()->className() <<
                      " action=contactaborted)!";
      }
      else
      {
        // Bad header sent in SLP fields, or invitation type was not supported
        kWarning() << "Our invitation was not supported "
                      "or the contact client had an internal error "
                      "(contact=" << getContactHandle() <<
                      " session=" << sessionID_ <<
                      " class="   << metaObject()->className() <<
                      " action=contactaborted)";
      }

      contactAborted( i18n("The contact rejected the invitation. An internal error occurred.") );
      break;


    // The invitation was declined
    case 603:
      setCurrentMessageType( P2P_MSG_SESSION_DECLINE );  // FIXME test if it's a transfer decline or not (like gotSlpOk()).
      sendP2PAck();

      // Contact Declined
      contactRejected();
      break;

    // Last option: tell the contact it sent an unknown message
    default:
      kWarning() << "P2P message can't be handled, "
                    "unsupported SLP response "
                    "(preamble=" << preamble <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=send0x08).";

      // Got an unknown SLP status code
      sendP2PAbort();

      // Scheduelle the application to terminate
      showEventMessage( i18n("The invitation was cancelled. The contact sent bad data, or KMess does not support it."), ChatMessage::CONTENT_APP_CANCELED, true );
  }
}



/**
 * @brief Called when a SLP transfer invitation was received.
 *
 * This is typically received when the contact initiated the file transfer session.
 * The Content-Type of the message <tt>application/x-msnmsgr-transreqbody</tt>.
 * The message contains meta-information about the contact's network configuration.
 *
 * A typical incoming message looks like:
 * @code
INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-transreqbody
Content-Length: 193

Bridges: TRUDPv1 TCPv1
NetID: -789490475
Conn-Type: IP-Restrict-NAT
UPnPNat: true
ICF: false
Hashed-Nonce: {754C0129-F286-4882-5793-70BA45D62B6A}
SessionID: 114164
SChannelState: 0
@endcode
 *
 * As of Windows Live Messenger 8.1, the message body looks like:
@code
Bridges: TRUDPv1 TCPv1 SBBridge TURNv1
NetID: 140808277
Conn-Type: Port-Restrict-NAT
TCP-Conn-Type: Port-Restrict-NAT
UPnPNat: false
ICF: false
Hashed-Nonce: {78F2E9F6-06B7-9080-1D83-A222F143F681}
SessionID: 2674976
SChannelState: 0
Capabilities-Flags: 1
@endcode
 *
 * Currently the <tt>Hashed-Nonce</tt> is ignored, a normal <tt>Nonce</tt> is sent back.
 *
 * This method attempts to create a server socket and send the possible connection options back to the contact.
 * The response message also contains a <tt>Listening</tt> field
 * and supported transport layers (<tt>Bridge</tt> field).
 * It optionally contains one of the following:
 * - a generated nonce, the internal/external IP addresses (<tt>IPv4Internal-Addrs</tt> and <tt>IPv4external-Addrs</tt> values), or
 * - a null nonce to indicate the file data must be sent over the switchboard connection.
 *
 * See gotSlpTransferResponse() for an example message.
 *
 * The other contact can decide to start a server socket instead (sending a second invitation message),
 * or transfer the file data over the switchboard.
 *
 * @param  slpMimeBody  The SLP message with the fields "Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF".
 */
01949 void P2PApplication::gotSlpTransferInvitation(const MimeMessage &slpMimeBody)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Invite message is a request to transfer the session.";
#endif

  // For some reason it's also possible to receive SLP transfer invitations from WLM8
  // with an entirely different callid while trying to setup a direct connection.
  // Perhaps some packet is stalled in the outgoing message channel?
  if( sessionID_ == 0 || getMode() == APP_MODE_ERROR_HANDLER )
  {
    kWarning() << "Received an invitation for a direct connection,"
                  " but no session is set up "
                  "(maybe an old session, "
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send603).";

    sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE);
    return;
  }


  // See if there is already a connection attempt pending.
  // This really happened with WLM8.1, somehow it sent multiple connection invites
  // the the contact, even for the same session ID.
  if( applicationList_->hasPendingConnections() )
  {
    kWarning() << "Received an invitation for a direct connection,"
                  " while a connection attempt is already in progress "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send603).";

    sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE);
    return;
  }


  // See if the connection already exists
  if( applicationList_->hasDirectConnection() )
  {
    kWarning() << "Received an invitation "
                  "to start a direct connection, but a direct connection is already available "
                  "(authorized=" << applicationList_->hasAuthorizedDirectConnection() <<
                  " contact="    << getContactHandle() <<
                  " session="    << sessionID_ <<
                  " class="      << metaObject()->className() <<
                  " action=closeconnection).";

    // Drop the previous connection. HACK: added for amsn 0.97.
    // aMsn 0.97 does some interesting things:
    // - DC's it hosted are directly closed after sending all file data.
    // - DC's it connected to are left open forever. P2P messages are still received at it,
    //   but somehow all responses are sent over the SB back as if amsn doesn't know it has a DC.
    applicationList_->getDirectConnection()->closeConnection();
  }


  // Abort if the file data was just received.
  // For some reason WLM8 sometimes sends the invitation directly after the file was sent.
  // First cause some invites to fail (don't accept/cancel).
  // Then accept one but send an "transfer accept" with invalid IP/port.
  // Not sure it can always be reproduced this way though.
  if( isWaitingState( P2P_WAIT_FOR_SLP_BYE ) )
  {
    kWarning() << "Received an invitation for "
                  "a direct connection, but expecting BYE instead "
                  "(file transfer just completed, "
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=slpcancel).";
    sendCancelMessage( CANCEL_ABORT );
    return;
  }


  // New in WLM8 reverse direct connection invites for msnobject transfer
  bool isReverseInvite = isUserStartedApp();
  if( isReverseInvite )
  {
    // Receiving invitations for your own session only happens for msnobject transfer at the moment.
    // It means KMess needs:
    // - some way to delay the data preparation acks
    // - the ability to detect if a SLP error was sent for the transfer INVITE only.
    // - fixes for new issues with the state tracking.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Received a reverse invitation to for a direct connection, we started the session.";
#endif
  }


  // Create the accept message
  MimeMessage acceptMessage;
  bool startListening = false;

  // Get network details
  QString localIp   ( getLocalIp() );
  QString externalIp( getExternalIp() );

  // Read the data from the message
  QString bridges        ( slpMimeBody.getValue( "Bridges"  ) );  // Transport layers the client supports
//   int     netID          = slpMimeBody.getValue("NetID").toInt(); // Some ID, unused
  QString connectionType ( slpMimeBody.getValue( "Conn-Type") );  // Suggested connection type
  QString hasUPnPNat     ( slpMimeBody.getValue( "UPnPNat"  ) );  // uses UPnP-enabled NAT router
  QString hasICF         ( slpMimeBody.getValue( "ICF"      ) );  // uses Internet Connection Firewall


  // Based on the contact's network configuration,
  // we choose how we want to send the file.
  //
  // The client can include a Hashed-Nonce field,
  // and we can respond with another Hashed-Nonce field.
  // This algorithm is not known, but including a normal Nonce field
  // in the accept message seams to work (the client has some fallback).

  // Listen at the internal port
  // The connection can be used for both the internal and external port
  // because it only accepts one incoming packet, the second one will be rejected.
#if P2PAPPLICATION_AVOID_DC_SERVER
  int listeningPort = 0;  // for debugging.
#else
  int listeningPort = applicationList_->addServerConnection();
#endif

  if( listeningPort != 0 )
  {
    // Get parameters for message
    nonce_ = KMessShared::generateGUID();  // used for authentication

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Generating Nonce value "
            << nonce_ << " for incoming connections.";
#endif

    acceptMessage.addField( "Bridge",    "TCPv1" );
    acceptMessage.addField( "Listening", "true"  );
    acceptMessage.addField( "Nonce",     nonce_  );

    if(localIp != externalIp)
    {
      acceptMessage.addField("IPv4Internal-Addrs", localIp    );
      acceptMessage.addField("IPv4Internal-Port",  QString::number(listeningPort) );
    }

    acceptMessage.addField("IPv4External-Addrs", externalIp );   // TODO: include other aliases
    acceptMessage.addField("IPv4External-Port",  QString::number(listeningPort) );

    // Inform the user about the file transfer progress.
    showTransferMessage( i18n("Awaiting connection at %1, port %2", externalIp, QString::number( listeningPort ) ) );

    // Prepare to send the nonce too when connection to other contact is established
    connect( applicationList_, SIGNAL(     connectionEstablished() ),   // when connected, send nonce
             this,             SLOT  ( slotConnectionEstablished() ));
    connect( applicationList_, SIGNAL(      connectionAuthorized() ),   // when authorized, send data
             this,             SLOT  (  slotConnectionAuthorized() ));
    connect( applicationList_, SIGNAL(          connectionFailed() ),   // when failed, revert to switchboard
             this,             SLOT  (      slotConnectionFailed() ));

    startListening = true;  // avoids double-checking certain conditions.
  }
  else if( isReverseInvite )
  {
    // Could not listen at a local port, send reject instead.
    kWarning() << "Could not listen at local port, "
                  "redirecting transfer to the switchboard "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send603).";

    // Note this doesn't kill the session.
    setWaitingState( P2P_WAIT_DEFAULT, 0 );  // avoid assertion.
    sendSlpError("603 Decline", invitationSessionID_, invitationContentType_, P2P_MSG_TRANSFER_DECLINE);
    return;
  }
  else
  {
    // Could not listen at a local port, use switchboard instead.
    kWarning() << "Could not listen at local port, "
                  "redirecting transfer to the switchboard "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=continue).";

    // This configuration sends each file over the switchboard:
    acceptMessage.addField( "Bridge",    "TCPv1" );
    acceptMessage.addField( "Listening", "false" );
    acceptMessage.addField( "Nonce",     "{00000000-0000-0000-0000-000000000000}");

    // Inform the user about the file transfer progress.
    showTransferMessage( i18n("Reverting to indirect file transfer (this could be slow).") );
    // TODO: at this point, it's assumend the other client will initiate the transfer.
    //       perhaps set the waitingState to detect when the contact does not return a request?
    //       Windows Live Messenger is also capable of sending a new INVITE message so it becomes the server instead.
  }

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Sending 200 transfer OK message, "
              "listening=" << (listeningPort != 0) << ".";
#endif

  // Confirm the session
  sendSlpOkMessage( acceptMessage );

  // Wait for the contact to ACK the message..
  if( startListening )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for a connection to succeed or fail...";
#endif

    setWaitingState( P2P_WAIT_FOR_CONNECTION, P2PAPPLICATION_TIMEOUT_DATA );
  }
  else
  {
    // Expecting file data now we've rejected the invite.
    // TODO: it's also possible another invite is sent, hoping for a second chance.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for contact to send file data...";
#endif

    setWaitingState( P2P_WAIT_FOR_FILE_DATA, P2PAPPLICATION_TIMEOUT_DATA );
  }
}



/**
 * @brief Called when a response to a SLP transfer message was received.
 *
 * The message body contains the IP address to connect to.
 * It can be received the following situations:
 * - We sent the transfer invitation with a sendSlpTransferInvitation() call.
 *   The contact sends the "200 OK" message with the ipaddress/port information.
 * - We sent a transfer response back with "Listening: false" field.
 *   The contact sends a second SLP INVITE to become the server instead.
 *
 * When the contact starts a server, it sends the following message:
 * @code
MSNSLP/1.0 200 OK
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-transrespbody
Content-Length: 144

Bridge: TCPv1
Listening: true
Nonce: {9BA191DB-3B68-93DA-91A2-BB9031D5B92F}
IPv4External-Addrs: 111.222.333.444
IPv4External-Port: 6891
IPv4Internal-Addrs: 192.168.0.111
IPv4Internal-Port: 6891
@endcode
 * As of Windows Live Messenger 8.0, a <code>SChannelState: 0</code> field is added.
 * Version 8.1 also adds a <code>Capabilities-Flags: 1</code>, <code>Conn-Type</code> and <code>TCP-Conn-Type</code> field.
 *
 * When the contact is not able to open a port, it sends the following message:
 * @code
MSNSLP/1.0 200 OK
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-transrespbody
Content-Length: 83

Bridge: TCPv1
Listening: false
Nonce: {00000000-0000-0000-0000-000000000000}
@endcode
 *
 * When the contact likes to become the server instead it sends the following message:
 * @code
INVITE MSNMSGR:user@kmessdemo.org MSNSLP/1.0
To: <msnmsgr:user@kmessdemo.org>
From: <msnmsgr:contact@kmessdemo.org>
...
Content-Type: application/x-msnmsgr-transrespbody
Content-Length: 146

Bridge: TCPv1
Listening: true
Hashed-Nonce: {B00B2A8A-B3B6-95EB-322F-379747842156}
IPv4Internal-Addrs: 10.0.0.152
IPv4Internal-Port: 1182
@endcode
 *
 * @param  slpMimeBody   The SLP message with the fields "Listening", "Nonce",
 *                       and optionally ipaddress and port information.
 * @param  secondInvite  Used to indicate the contact sent a second invitation.
 */
02245 void P2PApplication::gotSlpTransferResponse(const MimeMessage &slpMimeBody, bool secondInvite)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  if( ! secondInvite )
  {
    kDebug() << "Parsing 200 OK message.";
  }
  else
  {
    kDebug() << "Parsing 2nd transfer INVITE message, "
              << "contact offers to become the direct connection server.";
  }
#endif


  // Test for an existing (possibly even authorized) direct connection.
  // This could happen with local-lan file transfer, the roundtrip of the switchboard
  // response message could take longer then the time to authenticate the connection.
  if( applicationList_->hasDirectConnection() )
  {
    if( applicationList_->hasAuthorizedDirectConnection() )
    {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Connection already authorized "
                << "before invitation response was received, starting transfer.";
#endif
      initiateTransfer();
    }
    else
    {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Connection already established "
                << "before invitation response was received, waiting for connection to authorize...";
#endif
      // Slots already connected at this point.
    }

    return;
  }


  // Handle some special situations when the contact sent an INVITE message back instead.
  // This happens when:
  // - gotSlpTransferInvitation() was called become the contact wanted to initialize a direct connection.
  // - we sent a "200 OK" message back with "Listening: false" to indicate we can't be the server.
  // - this method gets called because the contact sends an INVITE to become the server instead.
  if( secondInvite )
  {
    // TODO: send SLP 200/OK message back when contact sent a second INVITE message?

    // The 'connectionAuthenticated()' and 'connectionFailed()' slots are connected in gotSlpTransferInvitation()
    // The 'connectionEstablished()' signal was not connected because we were supposed to become the server.
    connect( applicationList_, SIGNAL(     connectionEstablished() ),   // when connected, send nonce
             this,             SLOT  ( slotConnectionEstablished() ));
  }


  // The Conn-type field of the message is the least important here,
  // the "Listening", "ICF" and "UPnPNat" tell us more about the situation of the contact,
  // and the probability to create a successful direct connection in either way.

  // Get fields from message
  bool listening      = slpMimeBody.getValue("Listening") == "true";
  bool listenInternal = slpMimeBody.hasField("IPv4Internal-Addrs");
  bool listenExternal = slpMimeBody.hasField("IPv4External-Addrs");
  nonce_              = slpMimeBody.getValue("Nonce");

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "P2PApplication:gotSlpTransferResponse() - Storing contact nonce value "
            << nonce_ << " to authenticate later.";
#endif

#if P2PAPPLICATION_AVOID_DC_CLIENT
  listening = false;  // for debugging.
#endif


  // If the contact is not listening, we can do the following things:
  // - invite the contact to connect to connect to this host instead (not implemented yet, TODO)
  // - send the data over the switchboard instead. (always works, but it's slow).

  // If the contact is listening, try to create a direct connection to the contact.
  if( listening && (listenExternal || listenInternal) )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Contact is listening, "
              << "attempting to connect to the given IP-address.";
#endif

    // Get the IP addresses.
    QStringList externalIpAddresses;
    QStringList internalIpAddresses;
    quint16     externalPort = 0;
    quint16     internalPort = 0;

    if( listenInternal )
    {
      internalIpAddresses = slpMimeBody.getValue("IPv4Internal-Addrs").split(" ");
      internalPort        = (quint16)slpMimeBody.getValue("IPv4Internal-Port").toUInt();
    }

    if( listenExternal )
    {
      externalIpAddresses = slpMimeBody.getValue("IPv4External-Addrs").split(" ");
      externalPort        = (quint16)slpMimeBody.getValue("IPv4External-Port").toUInt();
    }

    // See if the clients are likely at the same lan.
    bool sameLan = ( listenInternal && externalIpAddresses.contains( CurrentAccount::instance()->getExternalIp() ) );

    // Avoid sending slotAllConnectionsFailed() if one connection attempt fails immediately.
    applicationList_->setAddingConnections( true );

    // Connect to the internal IP address
    if( listenInternal )
    {
      // Add connection attempt for each local IP address
      for ( QStringList::Iterator it = internalIpAddresses.begin(); it != internalIpAddresses.end(); ++it )
      {
        // Ignore when connection was made
        if( applicationList_->hasDirectConnection() )
        {
          break;
        }

        // Show "connecting to <internal ip>" message when connecting
        if( applicationList_->addConnection( *it, internalPort ) )
        {
          showTransferMessage( i18n("Connecting to %1, port %2", *it, QString::number( internalPort ) ) );
        }
      }
    }

    // Connect to the external IP address
    if( listenExternal )
    {
      // Add connection attempt for each external IP address
      for ( QStringList::Iterator it = externalIpAddresses.begin(); it != externalIpAddresses.end(); ++it )
      {
        // Ignore when connection was made
        if( applicationList_->hasDirectConnection() )
        {
          break;
        }

        // Show "connecting to <external ip>" message message unless the hosts are _likely_ at the same lan.
        // In that case, a previous "Connecting to ..." message should stay visible.
        if( applicationList_->addConnection( *it, externalPort ) && ! sameLan )
        {
          showTransferMessage( i18n("Connecting to %1, port %2", *it, QString::number( externalPort ) ) );
        }
      }
    }

    // Reset again.
    applicationList_->setAddingConnections( false );
  }


  // After passing the connections to addConnection() it's possible
  // some slots are already fired and the connection was established.
  if( applicationList_->hasDirectConnection() )
  {
    return;
  }


  // See if there aren't connections established already.
  if( applicationList_->hasPendingConnections() )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for connection attempt to succeed...";
#endif

    // Wait for connection to establish
    setWaitingState( P2P_WAIT_FOR_CONNECTION2, P2PAPPLICATION_TIMEOUT_DATA );
  }
  else
  {
    // Nothing is pending, nothing is connected.
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "No connection possible, "
              << "informing ApplicationList.";
#endif

    // If no direct connection attempt will be made.
    // Reset the pending invitation connection flag, ApplicationList sends the connectionFailed signal.
    // This also signals other applications the connection attempt failed.
    applicationList_->setPendingConnectionInvitation(false);
  }
}



/**
 * @brief Called when an ack is received, which is not handled internally.
 *
 * This method can be overwritten by the derived class, in case it needs to handle a specific ack message.
 *
 * @return Whether the ack was handled by this method.
 */
02446 bool P2PApplication::gotUnhandledAck( const P2PMessage &message, P2PMessageType ackedMessageType )
{
  Q_UNUSED( message );
  Q_UNUSED( ackedMessageType );

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Not handling the unknown ack";
#endif

  // Derived class can implement some handling.
  return false;
}



/**
 * @brief Notify the derived class it can initiate the file transfer.
 *
 * It either calls userStarted3_UserPrepares() or contactStarted4_ContactConfirmsPreparation(),
 * depending who started the application. The timeout timer will also be stopped.
 */
02467 void P2PApplication::initiateTransfer()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Signalling implementation class "
            << "to start the file transfer.";
#endif

  // No longer waiting, we're sending.
  if( isWaitingState( P2P_WAIT_FOR_CONNECTION )
  ||  isWaitingState( P2P_WAIT_FOR_CONNECTION2 ) )
  {
    setWaitingState( P2P_WAIT_DEFAULT, 0 );
  }

  // When we start sending information, it could take a while before
  // the contact returns messages which disable the waiting timer.
  // Avoid this by disabling it now, before we kill our own transfer.
  if( stopWaitingTimer() )
  {
    kWarning() << "timeout detection timer was still running, stopped "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=continue).";
  }


  // Call the connect implementation based on who invoked this application.
  // The implementations of these methods can call sendData(), and
  // the actual transfer will be handled transparently here.
  if( isUserStartedApp() )
  {
    userStarted3_UserPrepares();
  }
  else
  {
    contactStarted4_ContactConfirmsPreparation();
  }
}



/**
 * @brief Send a cancel message.
 *
 * The application will be terminated later, either directly or after the contact ACKed the error.
 *
 * The following cancelReason values are supported:
 * - <tt>CANCEL_INVITATION</tt>
 *     The user declined the invitation. This sends an SLP <tt>603 Decline</tt> message.
 *
 * - <tt>CANCEL_ABORT</tt> / <tt>CANCEL_FAILED</tt>
 *     The user aborted the session.
 *     If called from contactStarted1_ContactInvitesUser() with <tt>CANCEL_FAILED</tt>,
 *     this sends an SLP <tt>500 Internal Error</tt> message.
 *     Otherwise it sends a SLP BYE or P2P error message.
 *
 * - <tt>CANCEL_NOT_INSTALLED</tt>
 *     The requested service/application is not installed.
 *     This sends an SLP <tt>500 Internal Error</tt> message.
 *
 * - <tt>CANCEL_TIMEOUT</tt>
 *     There was a timeout waiting for the contact to accept or send certain P2P data.
 *     This sends an P2P message with the "waiting flag" set (type 4) and terminates the application direclty.
 *     This reason is used internally by certain timeout functions.
 *
 * - <tt>CANCEL_INVALID_SLP_CONTENT_TYPE</tt>
 *     Sends an "MSNSLP 500 Internal Error" message back, but with a <tt>null</tt> Content-Type value set.
 *
 * The error messages which send an SLP message can only be used from
 * contactStarted1_ContactInvitesUser() and userStarted2_ContactAccepts().
 * Once the MSNSLP error message is sent, P2PApplication waits for the
 * contact to ACK, and terminates the application afterwards.
 * Hence, you don't have to call endApplication() at all.
 *
 * When an MSNSLP error is sent at a different moment, this will likely result in
 * undefined responses from the other client (or the error message could be ignored).
 * If KMess is compiled in debug mode, an assertion in sendSlpError() detects this.
 *
 * The <tt>CANCEL_ABORT</tt> reason is the only valid response to reject
 * a data-preparation message in the userStarted3_UserPrepares() method.
 * If KMess is compiled in debug mode, an assertion will be triggered otherwise.
 *
 * @param  cancelReason  The reason why the application is cancelled.
 */
02553 void P2PApplication::sendCancelMessage(const ApplicationCancelReason cancelReason)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "request to abort "
            << "(reason=" << cancelReason << ", state=" << (int) getWaitingState() << ").";
#endif

  // I don't care what the reason is if you had
  // to send the data-preparation ACK
  if( userShouldAcknowledge_ )
  {
#ifdef KMESSTEST
    KMESS_ASSERT( cancelReason == CANCEL_ABORT);
#endif

    // Cancelled the data preparation message
    sendP2PAbort();
    userShouldAcknowledge_ = false;

    // TODO: send bye here to? wait for bye? just terminate?
    kWarning() << "data-preparation stage failed "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=endapplicationlater).";
    endApplicationLater();
    return;
  }


  switch(cancelReason)
  {
    // Declined the invitation
    case CANCEL_INVITATION:
#ifdef KMESSTEST
      KMESS_ASSERT( ! invitationContentType_.isEmpty() );
      KMESS_ASSERT( invitationSessionID_ != 0 );
#endif

      // Use the cancel-reason to send the correct message:
      sendSlpError("603 Decline", invitationSessionID_, invitationContentType_);
      return;


    // Timeout waiting for some P2P data
    // ...or timeout waiting for contact to accept.
    case CANCEL_TIMEOUT:
      // Timeout handling occurs via slotCleanup(), not here.
      kWarning() << "CANCEL_TIMEOUT is not supported here "
                    "(state="   << (int) getWaitingState() <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=send0x04,endapplicationlater)";

      // Send the low-level error message that the session is waiting for something!
      sendP2PWaitingError();
      endApplicationLater();
      return;


    // The application type is not installed
    case CANCEL_NOT_INSTALLED:
      if( gotSlpMessage_)
      {
        // Usually this particular error is invoked from Application::contactStarted1_ContactInvitesUser().
        sendSlpError("500 Internal Error", invitationSessionID_, "null");
      }
      else
      {
        kWarning() << "CANCEL_NOT_INSTALLED is not supported here "
                      "(state="   << (int) getWaitingState() <<
                      " contact=" << getContactHandle() <<
                      " session=" << sessionID_ <<
                      " class="   << metaObject()->className() <<
                      " action=return)";
      }
      return;


    // User or application wants to abort
    case CANCEL_ABORT:
    case CANCEL_FAILED:
      // Make sure sending is aborted.
      abortDataSending();

      // Determine which error message to send back.
      if( gotSlpMessage_ )
      {
        // A: Special case for SLP messages.
        if( isWaitingState( P2P_WAIT_USER_ACCEPT ) && cancelReason == CANCEL_ABORT )
        {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
          kDebug() << "Cancelling after receiving an SLP INVITE, "
                    << "sending BYE back.";
#endif
#ifdef KMESSTEST
          KMESS_ASSERT( isUserStartedApp() );
#endif
          // Send bye early, contact should detect this.
          sendSlpBye();
        }
        else
        {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
          kDebug() << "Aborting after receiving an SLP message, "
                    << "sending 500 Internal Error back.";
#endif
          // Cancelled while in SLP negotiate mode.
          // Include the original content-type to tell the other
          // client that part of the message was not in error.
          sendSlpError("500 Internal Error", invitationSessionID_, invitationContentType_);
        }
      }
      else if( isUserStartedApp() )
      {
        // B: User started application
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
        kDebug() << "P2PApplication::sendCancelMessage() Cancelling an application "
                  << "we've started ourselves, sending BYE early.";
#endif
        // Mark the logs if there is something weird going on.
        // It's possible the contact doesn't receive the INVITE anymore
        // while KMess can't detect yet it's closed.
        // This happens sometimes when KMess sent a file before which was aborted by WLM.
        if( hasUnAckedMessage( P2P_MSG_SESSION_INVITATION ) )
        {
          kWarning() << "aborting while INVITE ack was not received "
                        "(state="   << (int) getWaitingState() <<
                        " contact=" << getContactHandle() <<
                        " session=" << sessionID_ <<
                        " class="   << metaObject()->className() <<
                        " action=continue,sendslpbye).";
        }

        // Send bye early, contact should detect this.
        sendSlpBye();
      }
      else
      {
        // C: Contact started application
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
        kDebug() << "P2PApplication::sendCancelMessage() Cancelling an application "
                  << "started by the other contact, sending BYE early.";
#endif
        // Send bye early, contact should detect this.
        sendSlpBye();

        //  TODO: after the message is ACKed, the contact sends a 0x40 "I've sent the bye early" message.
        // KMess does not do this here.
      }

      return;


    // Invalid content type received
    case CANCEL_INVALID_SLP_CONTENT_TYPE:
#ifdef KMESSTEST
      KMESS_ASSERT( gotSlpMessage_ );
#endif
      // Send an 500 error message without content-type set
      sendSlpError("500 Internal Error");
      return;


    // Last option, unknown cancel message
    default:
      kWarning() << "unknown cancelReason used "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=send0x08).";

      // Initiate primitive abort.
      sendP2PAbort();
  }
}



/**
 * @brief Send the data preparation ACK.
 *
 * You need to call this method from userStarted3_UserPrepares()
 * to accept the data-preparation message.
 *
 * This method will actually set a flag only,
 * which is inspected by gotDataPreparation() when userStarted3_UserPrepares() returns.
 */
02742 void P2PApplication::sendDataPreparationAck()
{
  if(userShouldAcknowledge_)
  {
    // This value is checked again after the
    // userStarted3_UserPrepares() method returns.
    userShouldAcknowledge_ = false;
  }
  else
  {
    kWarning() << "Call not expected here! "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=continue)";
  }
}



/**
 * @brief Send a SLP BYE message to close the session.
 *
 * This should only be called if we sent the SLP INVITE too,
 * otherwise the contact should sent it.
 *
 * Once the BYE message is sent, it waits for the last ACK to arrive.
 * The session is terminated afterwards. If the last ACK doesn't arrive
 * before a timeout occurs, the application is terminated as well to avoid.
 *
 * See gotSlpBye() for an example of a SLP BYE message.
 */
02774 void P2PApplication::sendSlpBye()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Sending SLP BYE message.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( sessionID_ != 0 );
  KMESS_ASSERT( ! callID_.isEmpty() );
  KMESS_ASSERT( ! branch_.isEmpty() );
#endif

  // Handle
  QString myHandle( CurrentAccount::instance()->getHandle() );

  // Determine content.
  // In the last, the official client only had "\r\n\0" as content (length = 3).
  QString contentType   ( "application/x-msnmsgr-sessionclosebody" );
  QString content       ( "SessionID: " + QString::number( sessionID_ ) + "\r\n\r\n\0" );
  uint    contentLength = content.length();

  // Create the message
  QString slpMessage;
  slpMessage = "BYE MSNMSGR:" + getContactHandle() + " MSNSLP/1.0\r\n"
               "To: <msnmsgr:" + getContactHandle() + ">\r\n"
               "From: <msnmsgr:" + myHandle + ">\r\n"
               "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n" // from our INVITE
               "CSeq: 0\r\n"
               "Call-ID: " + callID_ + "\r\n"  // from our INVITE
               "Max-Forwards: 0\r\n"
               "Content-Type: " + contentType + "\r\n"
               "Content-Length: " + QString::number(contentLength) + "\r\n"
               + (contentLength > 0 ? "\r\n" : "")
               + content;

  // Send the message
  sendSlpMessage(slpMessage, P2P_MSG_SESSION_BYE);
  setClosing(true);

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for BYE ACK...";
#endif

  // Don't run endApplication() yet, there is one ACK we wait for...
  setWaitingState( P2P_WAIT_FOR_SLP_BYE_ACK, P2PAPPLICATION_TIMEOUT_ACK );
}



/**
 * @brief Send an SLP error response.
 *
 * This can be done to decline a file tranfer or tell the other client it sent bad SLP data.
 *
 * The session ID can be 0 to indicate there was an error in the Content-Type. Setting the
 * session ID and Content-Type to the values used in the INVITE message indicates
 * the invitation is cancelled.
 *
 * @param  statusLine          The status line, for example <tt>200 OK</tt>, or <tt>603 Decline</tt>.
 * @param  sessionID           The identifier of the current session (can be zero).
 * @param  messageContentType  Contact type of the message which is in error (can be empty)
 * @param  messageType         The message type. This is used later when an ACK is received.
 */
02836 void P2PApplication::sendSlpError( const QString &statusLine,
                                   const ulong sessionID, const QString &messageContentType, P2PMessageType messageType )
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Sending SLP error response: " << statusLine;
#endif

#ifdef KMESSTEST
  // Test, as other invocations likely result in undefined client behavour
  KMESS_ASSERT(   gotSlpMessage_ );
  KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) || isWaitingState( P2P_WAIT_USER_ACCEPT ) );
  KMESS_ASSERT( ! callID_.isEmpty() );
  KMESS_ASSERT( ! branch_.isEmpty() );
#endif

  QString myHandle( CurrentAccount::instance()->getHandle() );

  // Determine Content-Type and Content-Length fields
  QString content;
  QString contentType;
  uint    contentLength;

  if( callID_.isEmpty() || branch_.isEmpty() )
  {
    kWarning() << "attempting to send SLP error back, "
                  "but Call-ID or Branch is unknown! "
                  "(contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=send0x08)";

    sendP2PAbort();
    return;
  }

  // It's safe now to send the normal ACK.
  // Send it if this was not done by the caller yet.
  // The if(..) is not needed but avoids a NOTICE in the debug log.
  if( ! isP2PAckSent() )
  {
    sendP2PAck();
  }

  if(sessionID == 0)
  {
    // Used to indicate a general error
    content       = QString::null;
    contentType   = "null";
    contentLength = 0;
  }
  else
  {
#ifdef KMESSTEST
    KMESS_ASSERT(messageContentType != 0 && ! messageContentType.isEmpty());
#endif
    // Used to indicate an error in the Invite, or decline a session
    content       = "SessionID: " + QString::number(sessionID) + "\r\n\r\n\0";
    contentLength = content.length();
    contentType   = messageContentType;

    // TODO see when this should be application/x-msnmsgr-session-failure-respbody instead.
  }

  // Create the message
  QString slpMessage;
  slpMessage = "MSNSLP/1.0 " + statusLine + "\r\n"
               "To: <msnmsgr:" + getContactHandle() + ">\r\n"
               "From: <msnmsgr:" + myHandle + ">\r\n"
               "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n"  // Indicates which INVITE was in error.
               "CSeq: 0\r\n"
               "Call-ID: " + callID_ + "\r\n"
               "Max-Forwards: 0\r\n"
               "Content-Type: " + contentType + "\r\n"
               "Content-Length: " + QString::number(contentLength) + "\r\n"
               + (contentLength > 0 ? "\r\n" : "")
               + content;

  // Send the message
  sendSlpMessage(slpMessage, messageType);
  setClosing(true);

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for SLP ACK...";
#endif

  // Wait for the message to be ACK-ed.
  setWaitingState( P2P_WAIT_FOR_SLP_ERR_ACK, P2PAPPLICATION_TIMEOUT_ACK );
}



/**
 * @brief Utility function to send an SLP INVITE message.
 *
 * Derived classes use sendSlpSessionInvitation() and sendSlpTransferInvitation() instead.
 * Both functions use this method internally to send the invitation.
 *
 * This method is constructs the SLP headers for the INVITE message, and sends it using sendSlpMessage().
 * The Branch-ID is generated by this method, and the Call-ID is generated with the first request.
 * After sending the invitation, it's waiting for the contact to accept or decline the message.
 *
 * @param  message      The SLP body fields, typically 'SessionID', 'EUF-GUID', 'AppId', and 'Context'.
 * @param  contentType  Content-Type of the message body.
 * @param  messageType  The message type. This is used later when an ACK is received.
 */
02941 void P2PApplication::sendSlpInvitation(const MimeMessage &message, const QString &contentType, P2PMessageType messageType)
{
#ifdef KMESSTEST
  KMESS_ASSERT(! contentType.isEmpty()    );
  KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT ) );
#endif

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Sending INVITE message";
#endif

  // Mime message conversion
  QString mimeBody( message.getFields() );

  // Content handles
  QString myHandle( CurrentAccount::instance()->getHandle() );

  // Generate identifiers
  if( callID_.isNull() )
  {
    // The callID remains the same between multiple invites
    // It identifies the session.
    callID_ = KMessShared::generateGUID();
  }

  // BranchID identifies this INVITE request
  branch_ = KMessShared::generateGUID();

  // Create the message (space after CSeq is added to emulate msn exactly)
  QString slpMessage;
  slpMessage = "INVITE MSNMSGR:" + getContactHandle() + " MSNSLP/1.0\r\n"
               "To: <msnmsgr:" + getContactHandle() + ">\r\n"
               "From: <msnmsgr:" + myHandle + ">\r\n"
               "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n"
               "CSeq: 0 \r\n"
               "Call-ID: " + callID_ + "\r\n"
               "Max-Forwards: 0\r\n"
               "Content-Type: " + contentType + "\r\n"   // Starts MSNSLP Session.
               "Content-Length: " + QString::number(mimeBody.length() + 5) + "\r\n"
               "\r\n"
               + mimeBody +
               "\r\n";

  sendSlpMessage(slpMessage, messageType);

  // Keep the used content type in the state variables.
  // For contact-started invitations, this is used to store their type.
  // In this case, it can be used to store ours.
  invitationContentType_ = contentType;

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Waiting for contact to send INVITE ACK, and accept later...";
#endif

  // Wait for the contact to accept
  // MSN 6 had a timeout of 30 sec to accept, WLM seams to have no timeout.
  setWaitingState( P2P_WAIT_FOR_INVITE_ACK, P2PAPPLICATION_TIMEOUT_ACCEPT );
}



/**
 * @brief Send the 200 OK message to accept the invitation.
 *
 * The message should contain the 'SessionID' field. It's value can
 * be retreived with getInvitationSessionID().
 *
 * Once the confirmation message has been sent, this class will automatically
 * use the correct Session ID for following messages (since it was already read from the SLP INVITE message).
 *
 * See gotSlpOk() for a message example.
 * Note that the 'message' parameter only needs to supply the SLP body fields.
 * The SLP header will be added by this method.
 *
 * @param message  The SLP message body.
 */
03017 void P2PApplication::sendSlpOkMessage(const MimeMessage &message)
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Sending MSNSLP/1.0 200 OK message.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT(! branch_.isEmpty()                );
  KMESS_ASSERT(! invitationContentType_.isEmpty() );
  // Waiting state is P2P_WAIT_USER_ACCEPT if contactStarted2_.. is called directly from contactStarted1_..
  // TODO: handle WLM8 invitations sent before data preparation
  // SLP_WAIT_FOR_PREPARE happens when a confirmation needs to be sent when the session is still in prepare state
  KMESS_ASSERT( isWaitingState( P2P_WAIT_DEFAULT )        || isWaitingState( P2P_WAIT_USER_ACCEPT )
             || isWaitingState( P2P_WAIT_FOR_CONNECTION ) || isWaitingState( P2P_WAIT_FOR_PREPARE ) );
#endif

  // Get message content and Content-Type
  bool gotTransferInvitation = (invitationContentType_ == "application/x-msnmsgr-transreqbody");
  QString content    ( message.getFields() );
  QString contentType( (gotTransferInvitation ? "application/x-msnmsgr-transrespbody" : invitationContentType_) );

  QString myHandle( CurrentAccount::instance()->getHandle() );

  // Send the SLP OK message to tell the other client you'd like to start the transfer.
  QString slpMessage;
  slpMessage = "MSNSLP/1.0 200 OK\r\n"
               "To: <msnmsgr:" + getContactHandle() + ">\r\n"
               "From: <msnmsgr:" + myHandle + ">\r\n"
               "Via: MSNSLP/1.0/TLP ;branch=" + branch_ + "\r\n"
               "CSeq: " + QString::number(invitationCSeq_ + 1) + "\r\n"
               "Call-ID: " + callID_ + "\r\n"
               "Max-Forwards: 0\r\n"
               "Content-Type: " + contentType + "\r\n"
               "Content-Length: " + QString::number(content.length() + 3) + "\r\n"
               "\r\n"
               + content + "\r\n";

  sendSlpMessage(slpMessage, ( gotTransferInvitation ? P2P_MSG_TRANSFER_OK : P2P_MSG_SESSION_OK ) );


  // Initialize the session ID for the first OK message.
  // Note this method can be called again later to handle the transfer OK message.
  if( sessionID_ == 0 )
  {
    // Now that we've confirmed the session,
    // the sessionID will be used the next messages.
    sessionID_ = invitationSessionID_;
  }



  if(gotTransferInvitation)
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for contact to ACK the 200 transfer OK message...";
#endif

    // Wait until our transfer OK message is ACK-ed
    setWaitingState( P2P_WAIT_FOR_TRANSFER_ACK, P2PAPPLICATION_TIMEOUT_ACK );
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for contact to ACK the 200 OK message...";
#endif

    // Wait until our normal OK message is ACK-ed
    setWaitingState( P2P_WAIT_FOR_SLP_OK_ACK, P2PAPPLICATION_TIMEOUT_ACK );
  }
}



/**
 * @brief Send the invitation for a normal session.
 *
 * It sends a normal <tt>application/x-msnmsgr-sessionreqbody</tt> message using sendSlpInvitation().
 * The session ID is stored to handle incoming messages. It can be requested with getSessionID().
 *
 * Internally the remaining message fields are appended using:
 * - sendSlpInvitation()
 * - sendSlpMessage()
 * - sendP2PMessage()
 *
 * See gotSlpSessionInvitation() for an example message.
 *
 * @param  sessionID  The generated session ID, can be generated with generateID().
 * @param  eufGuid    The EUF-GUID value identifying the application type, from <tt>getAppId()</tt>.
 * @param  appId      The numeric app-Id value, which also identifies the application type.
 * @param  context    The context field.
 */
03107 void P2PApplication::sendSlpSessionInvitation( quint32 sessionID, const QString &eufGuid, const int appId, const QString &context )
{
  // Create the message
  MimeMessage invitation;
  invitation.addField( "EUF-GUID",  eufGuid );
  invitation.addField( "SessionID", QString::number(sessionID) );
  invitation.addField( "AppID",     QString::number(appId)     );
  invitation.addField( "Context",   context                    );

  // Send the invitation
  sendSlpInvitation( invitation, "application/x-msnmsgr-sessionreqbody", P2P_MSG_SESSION_INVITATION);

  // If the client accepts our invitation, we
  // use the session ID in the next ACK message.
  sessionID_        = sessionID;

  setObjectName( QLatin1String( metaObject()->className() ) +
                 "[" + QString::number( sessionID_ ) + "/" + getContactHandle() + "]" );
}



/**
 * @brief Send the invitation for a direct connection.
 *
 * This sends a <tt>application/x-msnmsgr-transreqbody</tt> message using sendSlpInvitation().
 * The contact responds with the ipaddresses/ports we can connect to.
 *
 * When the direct connection attempt completed, initiateTransfer() will be called.
 * It's possible the direct connection attempt failed, the clients will
 * exchange file data over the switchboard instead.
 *
 * See gotSlpTransferInvitation() for message examples.
 */
03141 void P2PApplication::sendSlpTransferInvitation()
{
  // Check if there is already an connection
  if( applicationList_->hasAuthorizedDirectConnection() )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Connection is already setup, "
              << "re-using the current connection.";
#endif
    initiateTransfer();
    return;
  }

  // Prepare to transfer when a connection is made by this application, or another application.
  connect( applicationList_, SIGNAL(      connectionAuthorized() ),   // when authorized, send data
           this,             SLOT  (  slotConnectionAuthorized() ));
  connect( applicationList_, SIGNAL(          connectionFailed() ),   // when failed, revert to switchboard
           this,             SLOT  (      slotConnectionFailed() ));

  // Check if there are pending connections
  if( applicationList_->hasPendingConnections() || applicationList_->hasPendingConnectionInvitation() )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Another application is already inviting "
              << "for a direct connection, waiting for that attempt to succeed or fail...";
#endif

    setWaitingState( P2P_WAIT_FOR_CONNECTION2, 0 );
    showTransferMessage( i18n("Waiting for connection") );
    return;
  }

  // Only connect last slot if we're really going to make the connection ourselves.
  connect( applicationList_, SIGNAL(     connectionEstablished() ),   // when connected, send nonce
           this,             SLOT  ( slotConnectionEstablished() ));

  // Invite the contact to create a connection.
  // This message tells the other client it can't connect to this end-host.
  // Either the data needs to be sent over the switchboard,
  // or the other client should initiate a direct connection server.
  MimeMessage invitation;
  invitation.addField("Bridges",      "TCPv1");
  invitation.addField("NetID",        QString::number( KMessShared::generateID() ));
  invitation.addField("Conn-Type",    "IP-Restrict-NAT");
  invitation.addField("UPnPNat",      "false");
  invitation.addField("ICF",          "false");

  // Send the invitation
  sendSlpInvitation( invitation, "application/x-msnmsgr-transreqbody", P2P_MSG_TRANSFER_INVITATION );

  // Wait for the ack, the application list should
  // timeout if the connections can't be made.
  setWaitingState( P2P_WAIT_FOR_INVITE_TR_ACK, 0 );

  // Mark invitation as pending.
  // This will fire an event when it's manually reset to false.
  applicationList_->setPendingConnectionInvitation( true );


  // When we include a Hashed-Nonce field, the contact also
  // adds a Hashed-Nonce to it's 'transrespbody' response message.
  // This may resemble http-digest authentication, but is still not researched.

  // Supported Conn-Type values:
  // - Firewall
  // - Direct-Connect     (Same IP, same port)
  // - Port-Restrict-NAT  (Same IP, different port)
  // - IP-Restrict-NAT    (Different IP, same port)
  // - Symmetric-NAT      (Different IP, different port)
  // - Unknown-Connect    (Different IP, different port)
  // The connection type can be discovered with STUN.

  // NetID is zero for Firewall/Direct-Connect,
  // otherwise it's a random number.

  // Inform the user about the file transfer progress.
  showTransferMessage( i18n("Negotiating options to connect") );
}



/**
 * Assign a fixed session ID (used for p2p ink transfers)
 */
03225 void P2PApplication::setDataCastSessionID( quint32 sessionID )
{
#ifdef KMESSTEST
  KMESS_ASSERT( sessionID_ == 0 );
  KMESS_ASSERT( sessionID < 600 );
  KMESS_ASSERT( getMode() == APP_MODE_DATACAST );
#endif

  sessionID_ = sessionID;
}



/**
 * @brief Called when the direct connection is authorized by one of the P2P applications.
 *
 * This initiates the transfer by calling initiateTransfer().
 */
03243 void P2PApplication::slotConnectionAuthorized()
{
  // Avoid posibility that this method is invoked again if
  // the direct connection is closed and re-connected later by another application.
  disconnect( applicationList_, 0, this, SLOT(slotConnectionAuthorized()));

  // The user who sent the INVITE, but the other side asked us to be the server.
  bool wasReverseInvite = ( isUserStartedApp() && applicationList_->getDirectConnection()->isServer() );

  if( wasReverseInvite )
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Direct connection authorized, "
                "reverse invitation complete (state=" << (int) getWaitingState() << ").";
#endif

    // Since we don't actively start reverse invitations,
    // assume for now that the contact should send something again.
    // TODO: when we send reverse invites too, we need to track who's turn it is.
    if( ! isTransferActive() )
    {
      // Waiting for prepare message
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Waiting for contact to send some prepare message...";
#endif

      setWaitingState( P2P_WAIT_FOR_PREPARE, P2PAPPLICATION_TIMEOUT_ACK );
    }

    // Do not call 'initiateTransfer()' here. For MsnObject transfers,
    // this will fire userStarted3 to confirm the data preparation.
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Direct connection authorized, "
                "starting transfer (state=" << (int) getWaitingState() << ").";
#endif

    // Protect against situations which could break our internal state.
    if( isTransferActive() )
    {
      kWarning() << "Direct connection authorized, "
                    "but data transfer was already started before. Won't notify derived classes "
                    "(state="   << (int) getWaitingState() <<
                    " contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " reverse=" << wasReverseInvite <<
                    " class="   << metaObject()->className() <<
                    " action=return).";
      return;
    }

    // No longer wait for packets, got connection, so go for it.
    stopWaitingTimer();
    initiateTransfer();
  }
}



/**
 * @brief Called when the direct connection is established.
 *
 * This sends the direct connection handshake with sendDirectConnectionHandshake(),
 * or waits for the contact to send one.
 */
03310 void P2PApplication::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
  kDebug() << "Direct connection established.";
#endif

  // Disconnect irrelevant signals.
  // Connection can still fail if the nonce is invalid.
  disconnect( applicationList_, 0, this, SLOT( slotConnectionEstablished() ));

  // Send the handshake if this is a new connection
  bool isClient = ( ! applicationList_->getDirectConnection()->isServer() );
  if( isClient )
  {
#ifdef KMESSTEST
    KMESS_ASSERT( isWaitingState( P2P_WAIT_FOR_CONNECTION2 ) );
#endif

    // Send the handshake
    sendDirectConnectionHandshake( nonce_ );

#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for direct connection handshake response...";
#endif

    // Start the timeout timer again.
    setWaitingState( P2P_WAIT_FOR_HANDSHAKE_OK, P2PAPPLICATION_TIMEOUT_ACK );
  }
  else
  {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
    kDebug() << "Waiting for contact to send connection handshake...";
#endif

    // Wait for the handshake
    setWaitingState( P2P_WAIT_FOR_HANDSHAKE, P2PAPPLICATION_TIMEOUT_ACK );
  }
}



/**
 * @brief Called when a direct connection could not be made.
 *
 * It initiates the transfer with initiateTransfer().
 */
03356 void P2PApplication::slotConnectionFailed()
{
  // Make this a warning, so users can figure out stuff when running kmess from the console.
  kWarning() << "A direct connection could not be made, "
                "switching file transfer to slow indirect transfer "
                "(state="   << (int) getWaitingState() <<
                " contact=" << getContactHandle() <<
                " session=" << sessionID_ <<
                " class="   << metaObject()->className() <<
                " action=continue).";

#ifdef KMESSTEST
  KMESS_ASSERT( ! applicationList_->hasDirectConnection()   );
  KMESS_ASSERT( ! applicationList_->hasPendingConnections() );
#endif

  // Disconnect all signals as they are not relevant anymore
  disconnect( applicationList_, 0, this, SLOT( slotConnectionAuthorized()  ));
  disconnect( applicationList_, 0, this, SLOT( slotConnectionEstablished() ));
  disconnect( applicationList_, 0, this, SLOT( slotConnectionFailed()      ));

  // Inform the user about the transfer progress
  showTransferMessage( i18n("Reverting to indirect file transfer (this could be slow).") );

  // start the transfer over the switchboard
  stopWaitingTimer();
  initiateTransfer();
}



/**
 * @brief Show a timeout message because an expected message was not received.
 *
 * The contact is already notified at this point about this problem.
 * All this method needs to do, is displaying a reasonable message at the console or user interface.
 */
03393 void P2PApplication::showTimeoutMessage( P2PWaitingState waitingState )
{
  // Important note:
  // Each case calling endApplication() contains a "return;"
  // All debug cases use "break;" so the final endApplication() is called.

  // Clear any active invitation links first
  modifyOfferMessage();

  // Put a message at the console
  switch( waitingState )
  {
    case P2P_WAIT_FOR_INVITE_ACK:
    {
      kWarning() << "Timeout waiting SLP INVITE ack message "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_CONTACT_ACCEPT:
    {
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Contact didn't accept the invitation, terminating manually.";
#endif

      // NOTE: WLM8 has no timeout abort P2P (file transfer) invitations.
      // It keeps the invitation open even if the switchboard closes.
      showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for the contact to accept."), ChatMessage::CONTENT_APP_CANCELED, false );
      sendCancelMessage( CANCEL_ABORT );  // Should call sendSlpBye().
      // Can't send 0x04 timeout message because there is no session yet, and previous message is already ACKed.
      // NOTE: it seams Windows Live Messenger 8.0 doesn't send a BYE ACK for this message.
      return;
    }

    case P2P_WAIT_FOR_CONNECTION:
    {
      showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for a connection to succeed or fail."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() );
      endApplication();
      break;
    }

    case P2P_WAIT_FOR_CONNECTION2:
    {
      // Still keep waiting, DirectConnectionBase should trigger a timeout.
      kWarning() << "Time-out waiting to connect to the remote system "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=return).";
      return;
    }

    case P2P_WAIT_FOR_SLP_OK_ACK:
    {
      kWarning() << "Timeout waiting for SLP OK ACK "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_PREPARE:
    {
      kWarning() << "Timeout waiting for data-preparation message "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_PREPARE_ACK:
    {
      kWarning() << "Timeout waiting for data-preparation ACK "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_TRANSFER_ACK:
    {
      kWarning() << "Timeout waiting for SLP transfer OK ACK "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_FILE_DATA:
    {
      kWarning() << "Timeout waiting for file data "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";

      // The 0x80 mesage should be sent now.

      // Set "incoming" parameter to display messages like "the file you're sending.." in the future.
      showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for data."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() );
      sendCancelMessage( CANCEL_FAILED );  // Should call sendSlpBye().
      return;
    }

    case P2P_WAIT_FOR_DATA_ACK:
    {
      kWarning() << "Timeout waiting for file data received ack "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_HANDSHAKE:
    {
      // NOTE: we could start an initiateTransfer() here, but this is clearly a client bug.
      kWarning() << "Timeout waiting for DC handshake "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_HANDSHAKE_OK:
    {
      // NOTE: we could start an initiateTransfer() here, but this is clearly a client bug.
      kWarning() << "Timeout waiting for DC handshake response "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      break;
    }

    case P2P_WAIT_FOR_SLP_BYE:
    {
      kWarning() << "Timeout waiting for BYE message "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      // No message required, already got what we wanted
      endApplication();
      return;
    }

    case P2P_WAIT_FOR_SLP_BYE_ACK:
    {
      kWarning() << "Timeout waiting for BYE-ACK "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      // No message required, already got what we wanted
      endApplication();
      return;
    }

    case P2P_WAIT_FOR_SLP_ERR_ACK:
    {
      kWarning() << "Timeout waiting for SLP Error-ACK "
                    "(contact=" << getContactHandle() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " action=endapplication).";
      // No message required, already got what we wanted
      endApplication();
      return;
    }

    default:
    {
      kWarning() << "Timeout waiting "
                    "(state="   << (int) getWaitingState() <<
                    " session=" << sessionID_ <<
                    " class="   << metaObject()->className() <<
                    " contact=" << getContactHandle() << ")";
    }
  }

  // Default implementation quits
  showEventMessage( i18n("The invitation was cancelled. A timeout occurred waiting for data."), ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() );
  endApplication();
}


/**
 * @brief Called when the user aborted the application.
 *
 * Displays a message using getUserAbortMessage() and notifies the contact.
 * Unlike the base class, it won't terminate the application since the contact should return an ACK message first.
 *
 * This method may be overwritten to add additional event handling.
 */
03596 void P2PApplication::userAborted()
{
#ifdef KMESSDEBUG_APPLICATION
  kDebug() << "user requests to abort the session. state=" << (int) getWaitingState();
#endif

  // Make sure a second message is not displayed here.
  if( isClosing() )
  {
    kWarning() << "Attempted to close application twice "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=endapplicationlater)";

    // Make sure the timer is started again.
    endApplicationLater();
    return;
  }
  else if( isWaitingState( P2P_WAIT_FOR_SLP_BYE ) )
  {
    // This is a problem when a client does not send a SLP BYE (as seen with amsn 0.97).
#ifdef KMESSDEBUG_APPLICATION
    kDebug() << "Ignoring request while waiting for SLP BYE.";
#endif

    return;
  }

  if( isTransferComplete() )
  {
    // Make sure the user won't get a "transfer complete" message
    // followed by a "contact aborted" message.
    //
    // Currently happens when a contact restarts a switchboard (aMsn 0.97 does).
    // The old switchboard is removed, and all applications abort.
    // TODO: The switchboard closeConnection() call assumes it's always user initiated.

    kWarning() << "not displaying message because data transfer is complete. "
                  "The switchboard was likely closed after receiving all data. "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=continue)";
  }
  else
  {
    // Display abort message
    modifyOfferMessage();
    showEventMessage( getUserAbortMessage(), ChatMessage::CONTENT_APP_CANCELED, false );
  }

  // Send cancel message.
  // Prepares the application to abort.
  sendCancelMessage( CANCEL_ABORT );

  // Set the state to avoid crashes.
  setUserAborted( true );
  setClosing( true );

  // Don't end the application, instead wait for contact's client to respond too
}



/**
 * @brief Called when the user rejected (declined) the application.
 *
 * Displays a message using getUserRejectMessage() and notifies the contact.
 * Unlike the base class, it won't terminate the application since the contact should return an ACK message first.
 */
03669 void P2PApplication::userRejected()
{
  if( isClosing() )
  {
#ifdef KMESSDEBUG_APPLICATION
    kWarning() << "Attempted to close application twice "
                  "(state="   << (int) getWaitingState() <<
                  " contact=" << getContactHandle() <<
                  " session=" << sessionID_ <<
                  " class="   << metaObject()->className() <<
                  " action=endapplicationlater)";
#endif

    // Make sure the timer is started again.
    endApplicationLater();
    return;
  }
  else
  {
#ifdef KMESSDEBUG_APPLICATION
    kDebug() << "user requests to reject the invitation.";
#endif

    // Send abort messages
    modifyOfferMessage();
    showEventMessage( getUserRejectMessage(), ChatMessage::CONTENT_APP_CANCELED, false );
    sendCancelMessage( CANCEL_INVITATION );

    // Set the state to avoid crashes.
    setUserAborted( true );
    setClosing( true );

    // Don't end the application, instead wait for contact's client to respond too
  }
}



#include "p2papplication.moc"

Generated by  Doxygen 1.6.0   Back to index