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

msnswitchboardconnection.cpp

/***************************************************************************
                          msnswitchboardconnection.cpp  -  description
                             -------------------
    begin                : Fri Jan 24 2003
    copyright            : (C) 2003 by Mike K. Bennett
    email                : mkb137b@hotmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "msnswitchboardconnection.h"

#include <qptrlist.h>
#include <kdebug.h>
#include <kextendedsocket.h>
#include <kfiledialog.h>
#include <qfileinfo.h>
#include <klocale.h>
#include <kaboutdata.h>
#include <kmessagebox.h>
#include <kurl.h>
#include <qcstring.h>
#include <qregexp.h>
#include <qfile.h>

#include "../chat/chatmessage.h"
#include "../contact/contactbase.h"
#include "../contact/invitedcontact.h"  // for cast
#include "../currentaccount.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "../msnobject.h"
#include "../emoticonmanager.h"
#include "msnnotificationconnection.h"
#include "chatinformation.h"
#include "mimemessage.h"
#include "p2pmessage.h"
#include "applications/applicationlist.h"

#ifdef KMESSDEBUG_SWITCHBOARD
  #define KMESSDEBUG_SWITCHBOARD_GENERAL
  #define KMESSDEBUG_SWITCHBOARD_P2P
  #define KMESSDEBUG_SWITCHBOARD_EMOTICONS
//   #define KMESSDEBUG_SWITCHBOARD_CONTACTS
//   #define KMESSDEBUG_SWITCHBOARD_KEEPALIVE
//   #define KMESSDEBUG_SWITCHBOARD_ACKS
#endif



// The constructor
MsnSwitchboardConnection::MsnSwitchboardConnection()
 : MsnConnection("MsnSwitchboardConnection"),
   abortingApplications_(false),
   acksPending_(0),
   autoDeleteLater_(false),
   backgroundConnection_(true),
   closingConnection_(false),
   connectionState_(SB_DISCONNECTED),
   initialized_(false),
   userStartedChat_(false),
   keepAliveTimer_(0),
   keepAlivesRemaining_(0)
{
  contactsInChat_.clear();
  pendingInvitations_.clear();
  pendingMessages_.setAutoDelete(true);
}



// The destructor
MsnSwitchboardConnection::~MsnSwitchboardConnection()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: entering destructor" << endl;
#endif

  // Close the connection
  closeConnection();

  emit deleteMe( this );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "DESTROYED Switchboard" << endl;
#endif
}



// Initialize or restart the last activity timer, for keep alive messages
void MsnSwitchboardConnection::activity()
{
  // Keepalives are not needed in multi-user chats.
  if( ! isConnected() || contactsInChat_.count() > 1 )
  {
    return;
  }

  // Initialize the timer on demand.
  if( keepAliveTimer_ == 0 )
  {
    keepAliveTimer_ = new QTimer( this, "keepalive-msgs" );

    // Disable keepalives for background switchboard connections. Instead, use the timer as a disconnection
    // timeout. It's also made server-side, but here it will save memory.
    if( backgroundConnection_ )
    {
      connect( keepAliveTimer_, SIGNAL( timeout() ), SLOT( timeoutConnection() ) );
    }
    else
    {
      connect( keepAliveTimer_, SIGNAL( timeout() ), SLOT( sendKeepAlive() ) );
    }
  }

  // Reset the number of remaining keep alives.
  if( ! backgroundConnection_ )
  {
    // A chat connection with a contact will be kept open at least for 15 minutes.
    keepAlivesRemaining_ = 18;
  }
  else
  {
    // We're much more strict when managing background connections. Make them last at most 5 minutes without activity.
    keepAlivesRemaining_ = 6;
  }

  // Reset the timer.
  keepAliveTimer_->stop();
  keepAliveTimer_->start( 50000, false );

#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
  kdDebug() << "MsnSwitchboardConnection::activity(): " << ( backgroundConnection_ ? "Background" : "Chatting" )
            << " keepalive session restarted." << endl;
#endif
}



// Clean the old unacked messages
void MsnSwitchboardConnection::cleanUnackedMessages()
{
  // Standard chat messages are sent with a 'ACK_NAK_ONLY' flag.
  // When the messages is received, nothing is sent.
  // When the messsage can't be delivered,. a 'NAK' is returned.
  // KMess caches the chat messages for 5 minutes to show the
  // "the following message could not be delivered:" messages.
  // This method cleans up that cache for messages that did not receive a NAK after 5 minutes.

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  uint mapCount = unAckedMessages_.count();
#endif

  uint minTime = QDateTime::currentDateTime().toTime_t() - ( 5 * 60 );

  // Find find the entries, then delete.
  QValueList<int> removeAcks;
  for( QMap< int, UnAckedMessage >::iterator it = unAckedMessages_.begin(); it != unAckedMessages_.end(); ++it )
  {
    // Get message info
    const UnAckedMessage &unAcked = it.data();
    if( unAcked.time < minTime )
    {
      // Message is expired, remove it.
      removeAcks.append( it.key() );
    }
  }


  // Remove list
  if( ! removeAcks.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
    QString join;
    for( QValueList<int>::iterator it = removeAcks.begin(); it != removeAcks.end(); ++it )
    {
      if( join.isEmpty() ) join += ",";
      join += QString::number( *it );
    }
    kdDebug() << "MsnSwitchboardConnection::cleanUnackedMessages: removing expired messages " << join << "." << endl;
#endif

    for( QValueList<int>::iterator it = removeAcks.begin(); it != removeAcks.end(); ++it )
    {
      unAckedMessages_.remove( *it );
    }
  }


#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kdDebug() << "MsnSwitchboardConnection::cleanUnackedMessages: "
            << "removed " << ( mapCount - unAckedMessages_.count() ) << " messages, "
            << "kept " << unAckedMessages_.count() << " messages until those expire." << endl;
#endif
}



// Close the connection
00208 void MsnSwitchboardConnection::closeConnection()
{
  // If there are still contacts, it means contactLeft() was not initiated,
  // and the user closed the chat window earlier.
  if( contactsInChat_.count() > 0 )
  {
    // Keep a default for re-connecting.
    lastContact_ = contactsInChat_[0];

    // Make sure all contacts are removed, which will also abort their applications in ApplicationList.
    QValueList<QString>::iterator it;
    for( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
    {
      ContactBase *contact = currentAccount_->getContactByHandle(*it);
      if(! KMESS_NULL(contact))
      {
        contact->removeSwitchboardConnection(this, true);  // could cause applications to abort.
      }
    }

    contactsInChat_.clear();
  }


  if( isConnected() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard::closeConnection: still connected, sending BYE." << endl;
#endif
    // Send a "bye"
    sendCommand( "BYE", "\r\n");
    disconnectFromServer();
  }

  // Reset state, so messages are queued when the connection
  // is back up but the contact is not yet in the chat.
  connectionState_ = SB_DISCONNECTED;
  closingConnection_ = false;

  // Also stop the activity timer
  if( keepAliveTimer_ != 0 )
  {
    keepAliveTimer_->stop();
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection::closeConnection(): Session ended." << endl;
#endif
}



// Clean up, close the connection, destroy this object
void MsnSwitchboardConnection::closeConnectionLater(bool autoDelete)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard::closeConnectionLater(): aborting applications nicely and closing connection." << endl;
#endif

  ContactBase *contact;
  bool hasAbortingApplications = false;
  closingConnection_ = true;

  // this method is called when the user wants to close the chat window (allowing everything to close nicely).
  // If there are no contacts in the chat, we can close directly.
  if( ! contactsInChat_.isEmpty() )
  {
    // There are still contacts.
    // Verify whether they still have applications running.
    // Allow those applications to abort, and report back when the connection can be closed.
    QPtrList<ContactBase> contactsToRemove;
    for( QStringList::Iterator it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
    {
      contact = currentAccount_->getContactByHandle(*it);
      if( ! KMESS_NULL(contact) && contact->hasApplicationList() )
      {
        ApplicationList *appList = contact->getApplicationList();
        bool aborting = appList->contactLeavingChat(this, true);
        if( aborting )
        {
          connect(appList, SIGNAL(     applicationsAborted(const QString&) ),
                  this,    SLOT  ( slotApplicationsAborted(const QString&) ));
          hasAbortingApplications = true;
        }
        else
        {
          contactsToRemove.append(contact);
        }
      }
    }

    // All contacts that don't need any aborting are removed now (not in the iterator loop)
    // The remaining ones are removed in slotApplicationsAborted().
    for( QPtrList<ContactBase>::Iterator it = contactsToRemove.begin(); it != contactsToRemove.end(); ++it)
    {
      contact = *it;
      if( ! KMESS_NULL(contact) )
      {
        contactsInChat_.remove( contact->getHandle() );
        contact->removeSwitchboardConnection( this, true );
      }
    }
  }


  if( hasAbortingApplications )
  {
    // Wait for all applications to abort.
    // Set variables to use in slotApplicationsAborted.
    abortingApplications_ = true;
    autoDeleteLater_      = autoDelete;
  }
  else
  {
    // No applications are aborting.
    // Close the connection directly
    closeConnection();

    // Automatically delete ourself
    if(autoDelete)
    {
      this->deleteLater();
    }
  }
}



// The socket connected, so send the version command.
00337 void MsnSwitchboardConnection::connectionSuccess()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Connected to server, sending authentication." << endl;
#endif
#ifdef KMESSTEST
  ASSERT( connectionState_ == SB_CONNECTING || connectionState_ == SB_REQUESTING_CHAT );
#endif

  connectionState_ = SB_AUTHORIZING;

  if( userStartedChat_ )
  { // This is a user-initiated chat
    // Set the usr information to the server.
    sendCommand( "USR", currentAccount_->getHandle() + " " + authorization_ + "\r\n" );
  }
  else
  { // This is a contact-initiated chat
    // Answer the chat with the authorization
    sendCommand( "ANS", currentAccount_->getHandle() + " " + authorization_ + " " + chatId_ + "\r\n" );
  }
}



// Do what's required when a contact joined
void MsnSwitchboardConnection::contactJoined(const QString& handle, const QString& friendlyName, const uint capabilities)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Contact " << handle << " has joined." << endl;
#endif

  // Update states
  connectionState_ = SB_CHAT_STARTED;

  // Add the contact to the list if the contact isn't there already
  if ( ! contactsInChat_.contains( handle ) )
  {
    contactsInChat_.append( handle );
  }

  // Indicate the contact is active in this session.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if( contact == 0 )
  {
    contact = currentAccount_->addInvitedContact(handle, friendlyName, capabilities);
  }

  // Add switchboard connection to the contact.
  if( ! KMESS_NULL(contact) )
  {
    contact->addSwitchboardConnection(this);
  }

  // Inform the contact about our version
  // Also do this when the contact left and re-entered the chat, it might connect with a different client.
  sendClientCaps();

  // Send all pending messages
  sendPendingMessages();

  // Notify the join to the Chat Master.
  emit contactJoinedChat( handle, friendlyName );
}



// Remove a contact from the list of contacts in the chat
void MsnSwitchboardConnection::contactLeft(const QString& handle)
{
#ifdef KMESSTEST
  ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Contact " << handle << " has left." << endl;
#endif

  QValueList<QString>::iterator it;

  // Check if the contact is in the chat..
  if( contactsInChat_.contains( handle ) )
  {
    // Find the contact in the chat
    for( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
    {
      if( (*it) == handle )
      {
        // Remove from the chat
        contactsInChat_.remove( it );
        break;
      }
    }
  }


#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection: emitting that '" << handle << "' left chat." << endl;
#endif

  // Indicate the contact is not active anymore in this session.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if(! KMESS_NULL(contact))
  {
    contact->removeSwitchboardConnection(this, false);
  }

  // Emit the signal to the Chat Window.
  if( ! backgroundConnection_ )
  {
    // If we're using the keepalive mechanism, and there are no keepalives left, then the session has expired:
    // the conversation went idle
    bool isKeepAliveEnabled = ( keepAliveTimer_ != 0 && keepAliveTimer_->isActive() );
    emit contactLeftChat( handle, isKeepAliveEnabled && keepAlivesRemaining_ < 1 );
  }
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  else
  {
    kdDebug() << "MsnSwitchboardConnection::contactLeft() - Not sending contactLeftChat() signal for background chats." << endl;
  }
#endif

  // Check if all contacts went away
  if( contactsInChat_.count() == 0 )
  {
    // Store contact to have a default when re-connecting.
    lastContact_ = handle;

    // The last contact left the chat.
    connectionState_ = SB_CONTACTS_LEFT;

    // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
    // Reset the list of sent emoticons, so they will be sent again if the chat restarts
    // sentEmoticons_.clear();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection: last contact left chat, closing connection." << endl;
#endif
    // No contacts left in the chat, close connection since it is of little use.
    // - the connection can still be used to 'CAL' the last contact again
    // - when the contact resumes the connection, it uses a different server,
    //   so startChat() needs to reconnect.
    closeConnection();
  }
}



// Convert an html format (#RRGGBB) color to an msn format (BBGGRR) color
void MsnSwitchboardConnection::convertHtmlColorToMsnColor(QString &color) const
{
#ifdef KMESSTEST
  ASSERT( color.length() == 7 );
#endif
  // Get the color components
  QString red   = color.mid(1, 2);
  QString green = color.mid(3, 2);
  QString blue  = color.mid(5, 2);

  // Reassemble the color
  if ( blue != "00" )
  {
    color = blue + green + red;
  }
  else if ( green != "00" )
  {
    color = green + red;
  }
  else if ( red != "00" )
  {
    color = red;
  }
  else
  {
    color = "0";
  }
#ifdef KMESSTEST
  ASSERT( color.length() < 7 );
#endif
}



// Convert and msn format color (BBGGRR) to an html format (#RRGGBB) color
void MsnSwitchboardConnection::convertMsnColorToHtmlColor(QString &color) const
{
#ifdef KMESSTEST
  ASSERT( color.length() < 7 );
#endif
  if ( color == "0" )
  {
    color = "#000000";
  }
  else
  {
    // Fill the color out to six characters
    while ( color.length() < 6 )
    {
      color = "0" + color;
    }
    // Get the color components
    QString blue  = color.mid(0, 2);
    QString green = color.mid(2, 2);
    QString red   = color.mid(4, 2);
    // Reassemble the components
    color = "#" + red + green + blue;
  }
#ifdef KMESSTEST
  ASSERT( color.length() == 7 );
#endif
}



// Make a list of the contacts in the chat
QStringList MsnSwitchboardConnection::getContactsInChat() const
{
  if( contactsInChat_.isEmpty() )
  {
    return QStringList( lastContact_ );
  }

  // Note this object may contain no contacts at all, and lastContact_ has the last one to re-invite.
  return contactsInChat_;
}



// Get a font from the message
void MsnSwitchboardConnection::getFontFromMessageFormat(QFont &font, const MimeMessage& message) const
{
  QString family  = message.getSubValue("X-MMS-IM-Format", "FN");
  QString effects = message.getSubValue("X-MMS-IM-Format", "EF");
  family = KURL::decode_string( family );

  font.setFamily( family );
  if ( effects.contains("B") )
    font.setBold(true);
  else
    font.setBold(false);
  if ( effects.contains("I") )
    font.setItalic(true);
  else
    font.setItalic(false);
  if ( effects.contains("U") )
    font.setUnderline(true);
  else
    font.setUnderline(false);
}



// Get an html font color from the message
void MsnSwitchboardConnection::getFontColorFromMessageFormat(QString &color, const MimeMessage& message) const
{
  color = message.getSubValue("X-MMS-IM-Format", "CO");
  convertMsnColorToHtmlColor( color );
}



// Return the first contact the chat started with.
const QString & MsnSwitchboardConnection::getFirstContact() const
{
  return firstContact_;
}


// Return the last contact who left the chat.
const QString & MsnSwitchboardConnection::getLastContact() const
{
  return lastContact_;
}



// Return whether the user started the chat
bool MsnSwitchboardConnection::getUserStartedChat() const
{
  return userStartedChat_;
}



// Received a positive delivery message.
void MsnSwitchboardConnection::gotAck(const QStringList& command)
{
#ifdef KMESSTEST
  ASSERT( unAckedMessages_.count() > 0 );
#endif

  // Remove the ACKed message from the queue.
  int ackNumber = command[1].toUInt();
  if( ! unAckedMessages_.contains(ackNumber) )
  {
    kdWarning() << "MsnSwitchboardConnection: Received an ACK message but message is not in sent queue." << endl;
    return;
  }

  unAckedMessages_.remove( ackNumber );
  acksPending_--;

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kdDebug() << "MsnSwitchboardConnection: Received one ACK message, still " << acksPending_ << " unacked." << endl;
#endif

  // Signal that the switchboard is no longer busy and can accept new application messages.
  if( acksPending_ < 2 )
  {
    emit readySend();
  }
}



// Received notification that a contact is no longer in session.
void MsnSwitchboardConnection::gotBye(const QStringList& command)
{
  contactLeft( command[1].lower() );
}



// Received the initial roster information for new contacts joining a session.
void MsnSwitchboardConnection::gotIro(const QStringList& command)
{
  QString handle       = command[4].lower();
  QString friendlyName = KURL::decode_string( command[5] );
  uint    capabilities = command[6].toUInt();

  QString altFriendlyName = currentAccount_->getContactFriendlyNameByHandle( handle );
  if( ! altFriendlyName.isEmpty() )
  {
    friendlyName = altFriendlyName;
  }

  contactJoined( handle, friendlyName, capabilities );
}



// Received notification of a new client in the session.
void MsnSwitchboardConnection::gotJoi(const QStringList& command)
{
  QString handle       = command[1].lower();
  QString friendlyName = KURL::decode_string( command[2] );
  uint    capabilities = command[3].toUInt();

  QString altFriendlyName = currentAccount_->getContactFriendlyNameByHandle( handle );
  if( ! altFriendlyName.isEmpty() )
  {
    friendlyName = altFriendlyName;
  }

  contactJoined( handle, friendlyName, capabilities );
}



// Received a negative acknowledgement of the receipt of a message.
void MsnSwitchboardConnection::gotNak(const QStringList& command)
{
#ifdef KMESSTEST
  ASSERT( unAckedMessages_.count() > 0 );
#endif

  // Check if the ACK exists in the map.
  int ackNumber = command[1].toUInt();
  if( ! unAckedMessages_.contains(ackNumber) )
  {
    kdWarning() << "MsnSwitchboardConnection: Received a NAK message but message is not in sent queue." << endl;
    return;
  }

  // Get message from queue and remove it.
  UnAckedMessage unAcked = unAckedMessages_[ackNumber];
  unAckedMessages_.remove(ackNumber);
  acksPending_--;

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kdDebug() << "MsnSwitchboardConnection: Received one NAK message, still " << acksPending_ << " unacked." << endl;
#endif



  // Signal that the switchboard is no longer busy and can accept new application messages.
  if( acksPending_ < 2 )
  {
    emit readySend();
  }

  // Do not notify errors for background connections
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kdDebug() << "MsnSwitchboardConnection::gotNak() - Not displaying undelivered message for background chats." << endl;
#endif
    return;
  }

  QString sender = unAcked.message.getValue( "To" );
  if( sender.isNull() )
  {
    sender = unAcked.message.getValue( "P2P-Dest" );
    if( sender.isNull() )
    {
      return;
    }
  }

  ContactBase *contact = CurrentAccount::instance()->getContactByHandle( sender );
  if( contact == 0 )
  {
    return;
  }

  QString friendlyName = contact->getFriendlyName();

  // Just be sure the switchboard is linked to a chat window.
  // For example, the user closes the chat and receives a NAK before the switchboard is closed.
  emit requestChatWindow( this );
  emit contactJoinedChat( sender, friendlyName );

  // Let the user know that a message wasn't delivered
  emit sendingFailed( unAcked.message.getBody() );
}



// Received notification of the termination of a client-server session.
void MsnSwitchboardConnection::gotOut(const QStringList& /*command*/)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - got OUT." << endl;
#endif

  closeConnection();
}



// Received a client-server authentication message.
void MsnSwitchboardConnection::gotUsr(const QStringList& command)
{
#ifdef KMESSTEST
  ASSERT( ! firstContact_.isEmpty() );
  ASSERT( connectionState_ == SB_AUTHORIZING );
#endif

  // This should just be a confirmation
  if ( command[2] != "OK" )
  {
    kdWarning() << "MsnSwitchboardConnection: Switchboard authentication failed" << endl;
    return;
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - authentication successful, inviting " << firstContact_ << endl;
#endif

  connectionState_ = SB_INVITING_CONTACTS;

  // Now call the other user to the conversation.
  sendCommand( "CAL", firstContact_ + "\r\n" );

  // If there are other pending invites, send them now
  if( pendingInvitations_.count() > 0 )
  {
    for( QStringList::Iterator it = pendingInvitations_.begin(); it != pendingInvitations_.end(); ++it )
    {
      inviteContact( *it );
      pendingInvitations_.remove( *it );
    }
  }
}



// Initialize the object
00815 bool MsnSwitchboardConnection::initialize()
{
  if ( initialized_ )
  {
    kdDebug() << "Switchboard Connection already initialized!" << endl;
    return false;
  }
  if ( !MsnConnection::initialize() )
  {
    kdDebug() << "Switchboard Connection: Couldn't initialize base class." << endl;
    return false;
  }

  initialized_ = true;
  return true;
}



// Invite a contact into the chat
void MsnSwitchboardConnection::inviteContact( QString handle )
{
  if( handle.isEmpty() )
  {
    return;
  }

  if( ! isConnected() )
  {
    // Request a new switchboard session
    emit requestNewSwitchboard( lastContact_ );
    // Add this contact to the list of pending invitations
    pendingInvitations_.append( lastContact_ );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection::inviteContact() - Added pending invitation for contact " << handle << endl;
#endif

    return;
  }

  sendCommand("CAL", handle + "\r\n");

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection::inviteContact() - Invited contact " << handle << endl;
#endif
}



// Check if a certain contact is in the chat
bool MsnSwitchboardConnection::isContactInChat( const QString& handle ) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kdDebug() << "MsnSwitchboardConnection::isContactInChat() - Checking handle " << handle << " - participants are: " << contactsInChat_.join(",") << " - lastContact: " << lastContact_ << endl;
  kdDebug() << "MsnSwitchboardConnection::isContactInChat() - returning " << ( ( (contactsInChat_.count() == 0) ? (lastContact_ == handle) : (contactsInChat_.contains( handle )) ) ? "true" : "false" ) << endl;
#endif

  return ( (contactsInChat_.count() == 0) ? (lastContact_ == handle) : (contactsInChat_.contains( handle )) );
}



// Check whether the switchboard is buzy (has too many pending messages)
bool MsnSwitchboardConnection::isBusy() const
{
  // unAckedMessages_ also contains messages what have a "NAK_ONLY" flag.
  // keep a special variable that only lists the normal ACKs.
  return acksPending_ > 3;
}



// Check if all contacts left
bool MsnSwitchboardConnection::isEmpty() const
{
  return contactsInChat_.empty();
}



// Check if only the given contact is in the chat
bool MsnSwitchboardConnection::isExclusiveChatWithContact(const QString& handle) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kdDebug() << "MsnSwitchboardConnection::isExclusiveChatWithContact() - Checking if chat is exclusive with " << handle
            << " (contacts=" << contactsInChat_.join(",") << ", lastContact=" << lastContact_ << ")" << endl;
#endif

  // Also check for last contact, contact can be re-invited to resume the session.
  // Previously, one contact was always left in the contactsInChat_ list.

  bool result = (contactsInChat_.count() == 1 && contactsInChat_[0] == handle)
             || (contactsInChat_.count() == 0 && lastContact_       == handle);

#ifdef KMESSDEBUG_SWITCHBOARD_CONTACTS
  kdDebug() << "MsnSwitchboardConnection::isExclusiveChatWithContact() - returning " << ( result ? "true" : "false" ) << endl;
#endif

  return result;
}



// Check whether the switchboard is currently not connected, nor trying to connect.
bool MsnSwitchboardConnection::isInactive() const
{
  return connectionState_ == SB_DISCONNECTED;
}



// Parse a regular command
00928 void MsnSwitchboardConnection::parseCommand(const QStringList& command)
{
  if ( command[0] == "ACK" )
  {
    gotAck( command );
  }
  else if ( command[0] == "ANS" )
  {
    // Do nothing.
  }
  else if ( command[0] == "BYE" )
  {
    gotBye( command );
  }
  else if ( command[0] == "CAL" )
  {
    // Do nothing
  }
  else if ( command[0] == "IRO" )
  {
    gotIro( command );
  }
  else if ( command[0] == "JOI" )
  {
    gotJoi( command );
  }
  else if ( command[0] == "NAK" )
  {
    gotNak( command );
  }
  else if ( command[0] == "OUT" )
  {
    gotOut( command );
  }
  else if ( command[0] == "USR" )
  {
    gotUsr( command );
  }
  else if ( command[0] == "216" || command[0] == "217" )
  {
    // Only send the message when we're linked to a ChatWindow.
    // There's no way of knowing from which contact call this error came from.
    if( backgroundConnection_ )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::parseCommand() - ignoring message 'the person is offline or invisible' in background chats." << endl;
#endif
      return;
    }

    emit chatMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                   ChatMessage::CONTENT_SYSTEM_NOTICE,
                                   true,
                                   i18n("This person is offline or invisible."),
                                   QString::null ) );
  }
  else if ( command[0] == "282" )
  {
    // Got it once when I sent a bad P2P message or something.
    kdDebug() << "Switchboard got unknown 282 error response." << endl;
  }
  else if ( command[0] == "911" )
  {
    kdDebug() << "Switchboard authentication failed." << endl;
  }
  else
  {
    kdDebug() << "Switchboard got unhandled command " << command[0] << " (contacts=" << contactsInChat_ << ")." << endl;
  }
}



// Parse a datacast message (e.g. nudge or voice clip)
void MsnSwitchboardConnection::parseDatacastMessage(const QString &contactHandle, const MimeMessage &message)
{
  int dataType = message.getValue("ID").toInt();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Parsing datacast message (ID=" << dataType << ")" << endl;
#endif

  // Get the contact
  ContactBase *contact = CurrentAccount::instance()->getContactByHandle( contactHandle );
  if( KMESS_NULL(contact) ) return;

  QString contactFriendlyName = contact->getFriendlyName();

  // Request a chat window when the chat is still in the background.
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: chat is still in the background, requesting chat window." << endl;
#endif
    backgroundConnection_ = false;
    emit requestChatWindow( this );
    emit contactJoinedChat( contactHandle, contactFriendlyName );
  }

  // Each ID has a different meaning.
  if(dataType == 1)
  {
    // A nudge
    emit receivedNudge(contactHandle);
  }
  else if(dataType == 2)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Datacast message contains an msn object (wink), signalling ChatMaster to download it." << endl;
#endif

    // A wink from a contact
    emit gotMsnObject(message.getValue("Data"), contactHandle);
  }
  else
  {
    // ID 3 is used for voice clips
    // ID 4 is used for action messages (MSNC6)
    // Not supported yet
    kdDebug() << "SwitchBoard got unhandled datacast message type (ID=" << dataType << " contact=" << contactHandle << ")." << endl;
    emit chatMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                   ChatMessage::CONTENT_SYSTEM_ERROR,
                                   true,
                                   i18n("The contact initiated a MSN7 feature KMess can't handle yet."),
                                   contactHandle,
                                   contactFriendlyName ) );
  }
}



// Parse an emoticon message
void MsnSwitchboardConnection::parseEmoticonMessage(const QString &contactHandle, const QString &messageBody)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection::parseEmoticonMessage() - Received custom emoticon list for " << contactHandle << "." << endl;
#endif

  QString emoticonCode;
  QString msnObjectData;

  // Get the contact
  ContactBase *contact = currentAccount_->getContactByHandle(contactHandle);
  if( KMESS_NULL(contact) ) return;

  // Request a chat window when the chat is still in the background.
  if( backgroundConnection_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::parseEmoticonMessage() - Chat is still in the background, requesting chat window." << endl;
#endif
    backgroundConnection_ = false;
    emit requestChatWindow( this );
    emit contactJoinedChat( contact->getHandle(), contact->getFriendlyName() );
  }

  // Emoticon data consists of a tab-separated list of pairs, each formed by an emoticon definition and the related msn object:
  // [Shortcut] TAB [MSN Object] TAB [Shortcut] TAB [MSN Object] TAB ...
  // |___first emoticon________|     |____second emoticon______|

  // Extract emoticon definitions and msn objects
  QStringList msnObjects = QStringList::split("\t", messageBody.stripWhiteSpace(), false);
  for( QStringList::Iterator it = msnObjects.begin(); it != msnObjects.end(); ++it )
  {
    emoticonCode = *it;

    ++it;

    // If the number of fields is odd, this custom emoticons list contains errors.
    if( it == msnObjects.end() )
    {
      kdWarning() << "MsnSwitchboardConnection::parseEmoticonMessage() - Emoticon message has an unexpected format: odd number of fields! "
                  << "(ignoring msnobject, contact=" << contactHandle << ")." << endl;
      break;
    }

    msnObjectData = *it;

    // Perform a syntax check.
    if( msnObjectData.length() < 20 )
    {
      kdWarning() << "MsnSwitchboardConnection::parseEmoticonMessage() - Emoticon message has an unexpected format "
                  << "(ignoring msnobject, contact=" << contactHandle << ", message='" << msnObjectData << "')." << endl;
      continue;
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::parseEmoticonMessage() - Adding emoticon code " << emoticonCode << "." << endl;
#endif

    // Store the emoticon code for the contact.
    MsnObject msnObject(msnObjectData);
    contact->addEmoticonDefinition(emoticonCode, msnObject.getDataHash());

    // Ask the ChatMaster to download emoticons.
    emit gotMsnObject(msnObjectData, contactHandle);
  }
}



// Parse a message command
01130 void MsnSwitchboardConnection::parseMessage(const QStringList& command, const MimeMessage &message)
{
  // Get the message type from the head
  QString contentType = message.getSubValue( "Content-Type" );
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Received mime message of type '" << contentType << "'." << endl;
#endif

  // Get the sender's handle and friendly name
  QString  contactHandle = command[1];
  QString  friendlyName;
  QString  contactPicture;

  // Get the contact details
  ContactBase *contact = currentAccount_->getContactByHandle( contactHandle );
  if( contact == 0 )
  {
    // There was no current friendly name, so get one from the message
    friendlyName = KURL::decode_string( command[2] );
  }
  else
  {
    // get name from contact
    friendlyName   = contact->getFriendlyName();
    contactPicture = contact->getContactPicturePath();
  }


  // Link a chat window to this switchboard if it's needed.
  if( backgroundConnection_ )
  {
    if( contentType == "text/plain"
    ||  contentType == "text/x-msmsgsinvite"
    ||  contentType == "text/x-mms-emoticon"
    ||  contentType == "text/x-mms-animemoticon"
    ||  contentType == "text/x-msnmsgr-datacast" )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "Switchboard: chat is still in the background, requesting chat window." << endl;
#endif
      backgroundConnection_ = false;
      emit requestChatWindow( this );
      emit contactJoinedChat( contactHandle, friendlyName );
    }
  }


  if( contentType == "text/plain" )
  {
    // This is a regular text message to the user
    QFont   font;
    QString color;
    if( message.hasField( "P4-Context" ) )
    {
      // The P4-Context field can override the default contact name.
      // It's typically used by plugins of the official client (e.g. Xiaoi's Qun).
      QString friendlyNameP4 = message.getValue( "P4-Context" );
      if( ! friendlyNameP4.isEmpty() )
      {
        friendlyName = friendlyNameP4;
      }
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Got message by " << contactHandle << ": " << message.getBody() << endl;
#endif

    // Get the font and color from the format string
    getFontFromMessageFormat     ( font,  message );
    getFontColorFromMessageFormat( color, message );

    // Send the chat message
    emit chatMessage( ChatMessage( ChatMessage::TYPE_INCOMING,
                                   ChatMessage::CONTENT_MESSAGE,
                                   true,
                                   message.getBody(),
                                   contactHandle,
                                   friendlyName,
                                   contactPicture,
                                   font,
                                   color ) );
  }
  else if( contentType == "text/x-msmsgscontrol" )
  {
    // Avoid crashes due race conditions
    if( closingConnection_ )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "Switchboard: Not emitting typing message because switchboard is closing." << endl;
#endif
      return;
    }

    // The contact informs it's typing a normal message
    QString typingUser = message.getValue( "TypingUser" );
    // Check the contact list for a more current friendly name
    QString typingName = currentAccount_->getContactFriendlyNameByHandle( typingUser );
    if( typingName.isEmpty() )
    {
      typingName = typingUser;
    }
    emit contactTyping( typingUser, typingName );
  }
  else if( contentType == "text/x-msmsgsinvite" )
  {
    // This is a mime application message, the old format for invitations.
    // Extract the actual MIME message from the body of the Mime container.
    MimeMessage subMessage( message.getBody() );
    emit gotMessage( subMessage, contactHandle );
  }
  else if( contentType == "application/x-msnmsgrp2p" )
  {
    // This is an p2p message, the new format for invitations.
    // First see if the message was direct to us (the switchboad is a "broadcast" channel for all messages)
    QString p2pDest = message.getValue("P2P-Dest");
    if(p2pDest.isEmpty())
    {
      // P2P dest is empty if we produce an error when a session is not initiated yet (also has To: <msnmsgr:> set)
      // Also observed with amsn 0.97 once.
      kdWarning() << "SwitchBoard: Unable to handle P2P message, P2P-Dest field is empty "
                     "(contact=" << contactHandle << ")." << endl;
      return;
    }
    else if(p2pDest != currentAccount_->getHandle())
    {
      // Ignore messages ment for other contacts
#ifdef KMESSDEBUG_SWITCHBOARD_P2P
      kdDebug() << "SwitchBoard: Received a P2P message, but it's for '" << p2pDest << "'." << endl;
#endif
      return;
    }

    // Extract the actual P2P message from the body of the Mime container.
    // Dispatch the message to the central ApplicationList of the Contact (maintained by ChatMaster).
    P2PMessage subMessage( message.getBinaryBody() );
    emit gotMessage( subMessage, contactHandle );
  }
  else if( contentType == "text/x-mms-emoticon" || contentType == "text/x-mms-animemoticon" )
  {
    // Message contains the MSN objects for the emoticons.
    parseEmoticonMessage( contactHandle, message.getBody() );
  }
  else if( contentType == "text/x-msnmsgr-datacast" )
  {
    // This is a datacast message, contact wants to send a nudge or voice clip
    MimeMessage subMessage( message.getBody() );
    parseDatacastMessage( contactHandle, subMessage );
  }
  else if( contentType == "text/x-clientcaps" )
  {
    // This is a message exchanged by a lot of third party clients.
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Got third-party client info message. (message dump follows)\n" << message.getMessage().data() << endl;
    // Client-Name: Client-Name/Version-Major.Version-Minor
    // Chat-Logging: Y (Nonsecure logging is enabled), S (log is encrypted), N (client is not logging conversation)
#endif
  }
  else if( contentType == "text/x-keepalive" )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kdDebug() << "Switchboard: Keep alive message from " << contactHandle << "." << endl;
#endif
  }
  else
  {
    kdDebug() << "SwitchBoard got unhandled message type (type=" << contentType << " contact=" << contactHandle << ")." << endl;
  }

  // Messages which are part of the switchboard session management, will not be considered "activity"
  if( contentType != "text/x-clientcaps" )
  {
    // Signal presence of activity
    activity();
  }
}



// Parse a payload command
01309 void MsnSwitchboardConnection::parsePayloadMessage(const QStringList &command, const QByteArray &/*payload*/)
{
  // Switchboard has no payload commands yet.
  // This method is added because the functionality is generic in the base class.
  kdWarning() << "MsnSwitchboardConnection::parsePayloadMessage: Unhandled payload command: " << command[0] << "!" << endl;
}



// Deliver a message for an P2PApplication object.
void MsnSwitchboardConnection::sendApplicationMessage( const MimeMessage &message )
{
#ifdef KMESSTEST
  ASSERT( ! isBusy() );
#endif

  // Check which type of message is sent
  QString contentType = message.getValue("Content-Type");
  if( contentType == "application/x-msnmsgrp2p" )
  {
    // Message contains binary P2P message data
#ifdef KMESSTEST
    ASSERT( message.getBinaryBody().size() >= 52 );  // header is 48, footer is 4
#endif

    sendMimeMessageWhenReady( ACK_ALWAYS_P2P, message );
  }
  else if( contentType == "text/x-msmsgsinvite"
           ||  contentType.section(";", 0, 0) == "text/x-msmsgsinvite" )  // for ; charset= suffix
  {
    // Message contains old-style invitation fields.
    sendMimeMessageWhenReady( ACK_NAK_ONLY, message );
  }
  else
  {
    kdWarning() << "MsnSwitchboardConnection::sendApplicationMessage: unknown message type "
                   "'" << contentType << "', can't send message!" << endl;
    return;
  }

  // Signal the presence of activity on this switchboard
  activity();
}


/**
 * Send a message to the contact(s)
 *
 * If there is at least one custom emoticon in our message, also send a message to tell our contact's clients
 * that they have to download from us the corresponding pictures.
 */
01360 void MsnSwitchboardConnection::sendChatMessage(QString text)
{
  if ( currentAccount_ == 0 )
  {
    kdWarning() << "MsnSwitchboardConnection::sendChatMessage - currentAccount_ is null!" << endl;
    return;
  }

  // Check if any custom emoticon is being sent in the message.
  // This has to be sent first so the receiving client will be aware that
  // there will be custom emoticons in the next message.
  QString code, pictureFile;
  QString emoticonObjects;
  int lastPos                                = 0;
  int matchStart                             = 0;
  EmoticonManager *manager                   = EmoticonManager::instance();
  const QRegExp   &emoticonRegExp            = manager->getPattern( true );
  QMap<QString,QString> emoticonPictures     = manager->getFileNames( true );
  QString emoticonThemePath                  = manager->getThemePath( true );

  // We'll loop until we reach the end of the string or there are no more emoticons to parse
  if( ! emoticonRegExp.isEmpty() )
  {
    while( true )
    {
      // First find if there's any custom emoticon
      matchStart = emoticonRegExp.search( text, lastPos );
      if( matchStart == -1 )
      {
        break;
      }

      // Find out what emoticon has matched
      code = text.mid( matchStart, emoticonRegExp.matchedLength() );
      // Find where the emoticon code ends and the image corresponding to that code
      lastPos = matchStart + emoticonRegExp.matchedLength();
      pictureFile = emoticonPictures[ code ];

      /*
      // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
      // If we've already sent this emoticon in a previous message, don't send it again
      if( sentEmoticons_.contains( code ) )
      {
        continue;
      }
      else
      {
        sentEmoticons_.append( code );
      }
      */

      // No match? Strange.. but go on anyways
      if( pictureFile.isEmpty() )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
        kdDebug() << "MsnSwitchboardConnection::sendChatMessage() - Custom emoticon '" << code << "' not found!" << endl;
#endif

        continue;
      }

#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
      kdDebug() << "MsnSwitchboardConnection::sendChatMessage() - Found custom emoticon '" << code << "' which file is '"
                << ( emoticonThemePath + pictureFile ) << "'." << endl;
#endif

      QFile iFile( emoticonThemePath + pictureFile );
      if( ! iFile.open( IO_ReadOnly ) )
     {
#ifdef KMESSDEBUG_SWITCHBOARD_EMOTICONS
        kdDebug() << "MsnSwitchboardConnection::sendChatMessage() - Unable to read picture '" <<  pictureFile << "'!" << endl;
#endif
        iFile.close();
        continue;
      }

      // Read the file and create an MSNObject of the emoticon
      QByteArray data = iFile.readAll();
      iFile.close();
      MsnObject test( currentAccount_->getHandle(), pictureFile, QString::null, MsnObject::EMOTICON, data );

      // Only divide items between each other
      if( ! emoticonObjects.isEmpty() )
      {
        emoticonObjects += "\t";
      }

      emoticonObjects += code + "\t" + test.objectString();
    }
  }

  // Don't send the message if there is no emoticon to send
  if( ! emoticonObjects.isEmpty() )
  {
    MimeMessage emoticonMessage;
    emoticonMessage.addField("MIME-Version",    "1.0");
    emoticonMessage.addField("Content-Type",    "text/x-mms-emoticon");
    emoticonMessage.setBody( emoticonObjects );

    sendMimeMessageWhenReady( ACK_NAK_ONLY, emoticonMessage );
  }

  // Then send the real text message

  // Get parameters
  QFont   font       = currentAccount_->getFont();
  QString fontFamily = font.family();
  QString color      = currentAccount_->getFontColor();

  // Convert data
  fontFamily = KURL::encode_string( fontFamily );
  convertHtmlColorToMsnColor( color );

  // Determine effects
  QString effects    = "";
  if ( font.bold() )      effects += "B";
  if ( font.italic() )    effects += "I";
  if ( font.underline() ) effects += "U";

  // Determine text direction
  QString rtl = text.isRightToLeft() ? "; RL=1" : "";

  // Create the message
  MimeMessage message;
  message.addField("MIME-Version",    "1.0");
  message.addField("Content-Type",    "text/plain; charset=UTF-8");
  message.addField("X-MMS-IM-Format", "FN=" + fontFamily + "; EF=" + effects + "; CO=" + color + "; CS=0; PF=0" + rtl);
  message.setBody(text);

  // Send the message
  sendMimeMessageWhenReady(ACK_NAK_ONLY, message);

  // Signal the presence of activity on this switchboard
  activity();
}



// Send a client caps message to the contacts
void MsnSwitchboardConnection::sendClientCaps()
{
  // All third-party clients send this message.
  // It also makes debugging easier.
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-clientcaps");
  message.setBody( "Client-Name: KMess " + kapp->aboutData()->version() + "\r\n" );
  sendMimeMessageWhenReady( ACK_NONE, message );
}



// Send a "ping" to avoid MSN closing the connection
void MsnSwitchboardConnection::sendKeepAlive()
{
  // Stop the keepalive sending if it's passed too much time
  if( keepAlivesRemaining_ < 1 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kdDebug() << "MsnSwitchboardConnection::sendKeepAlive(): Session has expired, letting it timeout." << endl;
#endif

    if( keepAliveTimer_ != 0 )
    {
      keepAliveTimer_->stop();
    }

    return;
  }
  else
  {
    keepAlivesRemaining_--;
  }

  // Sending messages to keep a connection open makes no sense if there's no connection, isn't it
  if( ! isConnected() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kdDebug() << "MsnSwitchboardConnection::sendKeepAlive(): Cannot send keep alive while disconnected!" << endl;
#endif
    return;
  }

  // Also check if there actually is a receiver for the message
  if( contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kdDebug() << "MsnSwitchboardConnection::sendKeepAlive(): Not sending keep alive, no contacts in chat." << endl;
#endif
    return;
  }

#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
  kdDebug() << "MsnSwitchboardConnection::sendKeepAlive(): Sending a keep alive message (" << keepAlivesRemaining_ << " left before close)." << endl;
#endif

  // Build the keepalive message
  MimeMessage message;
  message.addField( "MIME-Version", "1.0" );
  message.addField( "Content-Type", "text/x-keepalive" );

  sendMimeMessage( ACK_NONE, message );
}



// Send a message to the contact(s), or leave it pending until a connection is restored
void MsnSwitchboardConnection::sendMimeMessageWhenReady(AckType ackType, const MimeMessage &message)
{
  // If the connection is ready, send the message
  if( isConnected() && ! contactsInChat_.empty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Sending mime message of type '" << message.getValue("Content-Type") << "'." << endl;
#endif
#ifdef KMESSTEST
    ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif

    // Send and store for acknowledgement.
    int ack = sendMimeMessage(ackType, message);
    storeMessageForAcknowledgement(ack, ackType, message);

    // Clean cache of old entries, it does not need to run with every storeMessage..() call.
    cleanUnackedMessages();
  }
  else
  {
    // otherwise, store as pending message
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Chat is not active, queueing message to send later." << endl;
#endif

    // Store the message
    pendingMessages_.append( new QPair<AckType,MimeMessage>(ackType, message) );


    // See what needs to be done to restore the chat.
    if( ! isConnected() )
    {
      // The connection was closed, request a new one from the notification server.
      // It's not possible to simply re-open it, because we need a new authcookie for the USR command.
      if( connectionState_ == SB_REQUESTING_CHAT )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Already waiting for MsnNotificationConnection to request a new chat." << endl;
#endif
      }
      else if( connectionState_ == SB_CONNECTING )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Already waiting for connection to establish..." << endl;
#endif
      }
      else if( connectionState_ == SB_CHAT_STARTED )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Contact disconnected while joining, delivering error message." << endl;
#endif
        // Do not notify errors for background connections
        if( backgroundConnection_ )
        {
#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
        kdDebug() << "MsnSwitchboardConnection::sendMimeMessageWhenReady() - Not displaying undelivered message for background chats." << endl;
#endif
        }
        else if( ! message.getValue( "To" ).isNull() )
        {
          // Tell the user that this message was not received
          emit chatMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                         ChatMessage::CONTENT_SYSTEM_ERROR,
                                         false,
                                         i18n("The message \"%1\" was not received!").arg( message.getBody() ),
                                         message.getValue( "To" ) ) );
        }
      }
      else
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Connection is closed, requesting a new chat from the notification server..." << endl;
#endif
#ifdef KMESSTEST
        ASSERT( connectionState_ == SB_DISCONNECTED );
#endif

        connectionState_ = SB_REQUESTING_CHAT;

        // Request a new switchboard session
        emit requestNewSwitchboard( lastContact_ );
      }
    }
    else
    {
      // Connected but contacts are not in the chat yet.
      // See if we need to invite the contacts
      if( connectionState_ == SB_AUTHORIZING )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Already waiting for the connection to authorize..." << endl;
#endif
      }
      else if( connectionState_ == SB_INVITING_CONTACTS )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard: Already waiting for contacts to join the chat..." << endl;
#endif
      }
      else
      {
#ifdef KMESSTEST
        ASSERT( connectionState_ == SB_CONTACTS_LEFT );
#endif
        if( contactsInChat_.count() > 1 )
        {
          kdWarning() << "Switchboard failed to re-connect; multiple contacts are left in chat!" << endl;
          return;
        }

        if( message.hasField("P2P-Dest") && message.getValue("P2P-Dest") != lastContact_ )
        {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
          kdDebug() << "Switchboard: All contacts left the chat, calling the contact from the P2P-Dest field." << endl;
#endif
          connectionState_ = SB_INVITING_CONTACTS;
          sendCommand( "CAL", message.getValue("P2P-Dest") + "\r\n");
        }
        else
        {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
          kdDebug() << "Switchboard: All contacts left the chat, calling the last contact left." << endl;
#endif

          if( lastContact_.isEmpty() )
          {
            kdWarning() << "Switchboard failed to re-connect; no contact left to invite!" << endl;
            return;
          }

          connectionState_ = SB_INVITING_CONTACTS;
          sendCommand( "CAL", lastContact_ + "\r\n");
        }
      }
    }
  }
}



// Send messages that weren't sent because a contact had to be re-called
void MsnSwitchboardConnection::sendPendingMessages()
{
#ifdef KMESSTEST
  ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif

  QPair<AckType,MimeMessage> *pendingMessage;

  // A contact should be connected already, but check to make sure
  // The switchboard should still be (re-)initializing
  if( contactsInChat_.isEmpty() )
  {
    kdWarning() << "MsnSwitchboardConnection::sendPendingMessages: No contacts available in the chat." << endl;
    return;
  }

  // Send all messages
  pendingMessage = pendingMessages_.first();
  while( pendingMessage != 0 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Sending pending messages, " << ( pendingMessages_.count() -1 ) << " remaining." << endl;
#endif

    // Send the message
    int ack = sendMimeMessage( pendingMessage->first, pendingMessage->second );
    storeMessageForAcknowledgement( ack, pendingMessage->first, pendingMessage->second );

    // get next message
    pendingMessage = pendingMessages_.next();
  }

  // Clear the pending messages
  pendingMessages_.setAutoDelete(true);
  pendingMessages_.clear();

  // Clean cache of old entries, it does not need to run with every storeMessage..() call.
  cleanUnackedMessages();
}



// The user is typing so send a typing message
void MsnSwitchboardConnection::sendTypingMessage()
{
  if( contactsInChat_.isEmpty() )
  {
    if( lastContact_.isEmpty() )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::sendTypingMessage: Not sending typing notification, no contacts in chat, nor one to re-invite." << endl;
#endif
      return;
    }


    // When we need to re-invite a contact to send the typing message, see if the contact is offline
    // This avoids repeated "the contact is offline" typing messages.
    const ContactBase *contact = currentAccount_->getContactByHandle(lastContact_);
    if( contact == 0 || contact->isOffline() )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::sendTypingMessage: Not sending typing notification, last contact is offline." << endl;
#endif
      return;
    }
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Sending typing notification." << endl;
#endif

  // Build the typing notification message
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-msmsgscontrol");
  message.addField("TypingUser",   currentAccount_->getHandle());

  // if disconnected, reconnect to send the typing message
  // (maybe the other contact still has it's window open)
  sendMimeMessageWhenReady(ACK_NONE, message);

  // Signal the presence of activity on this switchboard
  activity();
}



// An ApplicationList object indicated it aborted all it's applications.
void MsnSwitchboardConnection::slotApplicationsAborted(const QString &handle)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection::slotApplicationsAborted: All applications of contact '" << handle << "' have been aborted." << endl;
#endif
#ifdef KMESSTEST
  ASSERT( abortingApplications_ );
#endif

  // Remove the contact from the chat now.
  contactsInChat_.remove(handle);

  // Disconnect from signal source again.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if( ! KMESS_NULL(contact) )
  {
    if( ! KMESS_NULL(contact->getApplicationList()) )
    {
      disconnect(contact->getApplicationList(), SIGNAL(      applicationsAborted(const QString&) ),
                 this,                          SLOT  (  slotApplicationsAborted(const QString&) ));
    }

    // Remove the reference to the switchboard here or we could get crashes later!
    contact->removeSwitchboardConnection(this, true);
  }


  // If all contacts have aborted, close connection.
  if( contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::slotApplicationsAborted: No other contacts need to abort, closing connection." << endl;
#endif

    abortingApplications_ = false;
    closeConnection();

    // If the switchboard should clean up afterwards, do so.
    if( autoDeleteLater_ )
    {
      this->deleteLater();
    }
  }
  else
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::slotApplicationsAborted: Waiting for applications of " << contactsInChat_.count() << " other contacts to abort..." << endl;
#endif
  }
}



// Start a switchboard connection
void MsnSwitchboardConnection::connectTo( const ChatInformation &chatInfo )
{
#ifdef KMESSTEST
  ASSERT( currentAccount_ != 0 );
#endif

  if(isConnected())
  {
    if( ! contactsInChat_.isEmpty() && ! isExclusiveChatWithContact(chatInfo.getContactHandle()))
    {
      kdWarning() << "Switchboard is already connected, "
                  << "can't start new chat with '" << chatInfo.getContactHandle() << "'!" << endl;
      return;
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Resuming a connection with a different server, reconnecting." << endl;
#endif
    closeConnection();
  }

  // Remove any contact from this switchboard session.
  if( ! contactsInChat_.isEmpty() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "Switchboard - Deleting active contacts: " << contactsInChat_.join(",") << endl;
#endif

    // Make sure all contacts are removed, which will also abort their applications in ApplicationList.
    ContactBase *contact;
    QValueList<QString>::iterator it;
    for( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
    {
      if( ! KMESS_NULL( *it ) )
      {
        contactsInChat_.remove( *it );
        contact = currentAccount_->getContactByHandle( *it );
        contact->removeSwitchboardConnection(this, true);  // could cause applications to abort.
      }
    }
  }

  // Prepare the class for the new connection, setting some values to good defaults
  // sentEmoticons_.clear(); // NOTE: Behavior changed since WLM 8+, emoticon data must be sent every time.
  closingConnection_  = false;
  firstContact_       = // Assign chatInfo.getContactHandle() to both first and last contact.
  lastContact_        = chatInfo.getContactHandle();

  // If requested, start an offline connection instead of a normal one
  if( chatInfo.getType() == ChatInformation::CONNECTION_OFFLINE )
  {
#ifdef KMESSTEST
    ASSERT( connectionState_ == SB_DISCONNECTED );
#endif

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Initializing offline connection with " << chatInfo.getContactHandle() << "." << endl;
#endif

    // lastContact_ gets set even when making offline connections, so isExclusiveChatWithContact() returns true;
    // otherwise a chat window would be spawned for every offline message.

    backgroundConnection_ = false;
    return;
  }


#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << " - Switchboard - Initializing connection with " << chatInfo.getContactHandle()
            << ": Connecting to SB " << chatInfo.getIp() << ":" << chatInfo.getPort() << "." << endl;
#endif

  // Store information from the chatinfo object
  authorization_        = chatInfo.getAuthorization();
  chatId_               = chatInfo.getChatId();
  userStartedChat_      = chatInfo.getUserStartedChat();

  // Connect to the server.
#ifdef KMESS_NETWORK_WINDOW
  KMESS_NET_INIT(this, "SB " + chatInfo.getIp());
#endif

  connectionState_ = SB_CONNECTING;
  if( ! connectToServer( chatInfo.getIp(), chatInfo.getPort() ) )
  {
    kdWarning() << "Switchboard couldn't connect to the server." << endl;
    connectionState_ = SB_DISCONNECTED;
  }

  // Link a chat window to this switchboard if it's needed.
  if( backgroundConnection_ && chatInfo.getType() == ChatInformation::CONNECTION_CHAT )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: requesting chat window for initiated chat invitation." << endl;
#endif
    emit requestChatWindow( this );
    backgroundConnection_ = false;
  }
}



// Send a nudge to a contact
void MsnSwitchboardConnection::sendNudge()
{
  // Create the message
  MimeMessage message;
  message.addField( "MIME-Version", "1.0" );
  message.addField( "Content-Type", "text/x-msnmsgr-datacast" );
  message.setBody( "ID: 1\r\n" );  // ID 1 indicates it's a nudge

  // Sent the message, re-establishing the connection if it was lost.
  sendMimeMessageWhenReady( ACK_NAK_ONLY, message );

  // Signal the presence of activity on this switchboard
  activity();
}



// Store a message for later acknowledgement
void MsnSwitchboardConnection::storeMessageForAcknowledgement(int ack, AckType ackType, const MimeMessage& message)
{
  // don't store if the ack-type indicates so
  if(ackType == ACK_NONE) return;

  // Only update pending ack list when we'll always get an ack back.
  if(ackType == ACK_ALWAYS || ackType == ACK_ALWAYS_P2P)
  {
    acksPending_++;
  }

  // Create a record of the unacked message
  UnAckedMessage unAcked;
  unAcked.ackType = ackType;
  unAcked.time    = QDateTime::currentDateTime().toTime_t();
  unAcked.message = message;  // no problem with data size, uses shared reference.

  // Add to QMap
  unAckedMessages_.insert( ack, unAcked );

#ifdef KMESSDEBUG_SWITCHBOARD_ACKS
  kdDebug() << "Switchboard: Stored message for acknowledgement. "
            << "There are currently " << unAckedMessages_.count() << " messages kept, "
            << acksPending_ << " need to be ACKed." << endl;
#endif
}



// The connection has gone inactive, close it
void MsnSwitchboardConnection::timeoutConnection()
{
  // Stop the keepalive sending if it's passed too much time
  if( keepAlivesRemaining_ < 1 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_KEEPALIVE
    kdDebug() << "MsnSwitchboardConnection::timeoutConnection(): Session has expired, closing it." << endl;
#endif

    if( keepAliveTimer_ != 0 )
    {
      keepAliveTimer_->stop();
    }

    closeConnectionLater( true );
    return;
  }
  else
  {
    keepAlivesRemaining_--;
  }
}



#include "msnswitchboardconnection.moc"

Generated by  Doxygen 1.6.0   Back to index