Logo Search packages:      
Sourcecode: kmess version File versions

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 <klocale.h>
#include <kaboutdata.h>
#include <kmessagebox.h>
#include <qcstring.h>
#include <qregexp.h>
#include <qfile.h>


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

#ifdef KMESSDEBUG_SWITCHBOARD
  #define KMESSDEBUG_SWITCHBOARD_GENERAL
#endif



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



// The destructor
MsnSwitchboardConnection::~MsnSwitchboardConnection()
{
  // Close the connection
  closeConnection();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "DESTROYED Switchboard" << 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_GENERAL  
  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_GENERAL
    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_GENERAL
  kdDebug() << "MsnSwitchboardConnection::cleanUnackedMessages: "
            << "removed " << ( mapCount - unAckedMessages_.count() ) << " messages, "
            << "kept " << unAckedMessages_.count() << " messages until those expire." << endl;
#endif
}



// Close the connection
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;
}



// 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

  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 )
    {
      ContactBase *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)
    {
      ContactBase *contact = *it;
      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.
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)
{
  // 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();

  // Emit the signal to the Chat Window.
  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

  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;
      }
    }

    if( contactsInChat_.count() == 0 )
    {
      // The last contact left the chat.
      connectionState_ = SB_CONTACTS_LEFT;
    }
  }


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

  // Store contact to have a default when re-connecting.
  lastContact_ = handle;

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

  // Emit the signal to the Chat Window.
  emit contactLeftChat( handle );


  // Check if all contacts went away
  if( contactsInChat_.count() == 0 )
  {
 #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
}



// Get the switchboard to make a list of the contacts in the chat
QString MsnSwitchboardConnection::getCaption()
{
  QString  caption;
  QString  contactString;

  // Make a list of the contacts in the chat
  if( contactsInChat_.count() == 0 && ! lastContact_.isEmpty() )
  {
    // Pretend the chat is still active, it will re-connect when the user or contact starts typing again.
    return i18n("%1 - Chat")
           .arg( currentAccount_->getContactFriendlyNameByHandle(lastContact_) );
  }
  else if( contactsInChat_.count() == 1 )
  {
    return i18n("%1 - Chat")
            .arg( currentAccount_->getContactFriendlyNameByHandle(contactsInChat_[0]) );
  }
  else if ( contactsInChat_.count() == 2 )
  {
    return i18n("%1 and %2 - Chat")
            .arg( currentAccount_->getContactFriendlyNameByHandle(contactsInChat_[0]) )
            .arg( currentAccount_->getContactFriendlyNameByHandle(contactsInChat_[1]) );
  }
  else if ( contactsInChat_.count() > 2 )
  {
    return i18n("%1 et al. - Chat")
            .arg( currentAccount_->getContactFriendlyNameByHandle(contactsInChat_[0]) );
  }

  return i18n("Chat");
}



// Make a list of the contacts in the chat
const QStringList& MsnSwitchboardConnection::getContactsInChat() const
{
  // 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");

  removePercents( 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_;
}



// 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_GENERAL
  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] );
}



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

  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];
  QString friendlyName = command[2];
  uint    capabilities = command[3].toUInt();
  removePercents( friendlyName );

  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_GENERAL
  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();
  }

  // Tell the user a certain message was not received
  emit chatMessage( ChatMessage(ChatMessage::TYPE_SYSTEM,
                                i18n("The message \"%1\" was not received").arg( 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" );
}



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



// Invite a contact into the chat
void MsnSwitchboardConnection::inviteContact(QString handle)
{
#ifdef KMESSTEST
  ASSERT( connectionState_ == SB_CHAT_STARTED );
#endif

  if ( handle != "" )
  {
    // TODO: Before sending "CAL", restore the connection when it's not initialized.
    sendCommand("CAL", handle + "\r\n");
  }
}



// Check if a certain contact is in the chat
bool MsnSwitchboardConnection::isContactInChat(const QString& handle) const
{
  return contactsInChat_.contains(handle);
  // TODO: like with isExclusiveChatWithContact(), also check for lastContact_? contactsInChat_ may be empty
  // return ( ! handle.isEmpty() ) && ( contactsInChat_.contains( handle ) || ( contactsInChat_.isEmpty() && lastContact_ == 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_GENERAL
  kdDebug() << "Switchboard - 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.

  return (contactsInChat_.count() == 1 && contactsInChat_[0] == handle)
      || (contactsInChat_.count() == 0 && lastContact_       == handle);
}



// Parse a regular command
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" )
  {
    emit chatMessage( ChatMessage(ChatMessage::TYPE_SYSTEM, i18n("This person is online, but he or she is blocking you.")) );
  }
  else if ( command[0] == "217" )
  {
    emit chatMessage( ChatMessage(ChatMessage::TYPE_SYSTEM, i18n("This person is offline or invisible.")) );
  }
  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
  {
    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

  // 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,
                                  i18n("The contact initiated a MSN7 feature KMess can't handle yet.")) );
  }
}



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

  QString emoticonCode;
  QString msnObjectData;

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

  // Emoticon data consists of a tab-separated list of definitions and msn objects:
  // [Shortcut] TAB [MSN Object] TAB [Shortcut] TAB [MSN Object] TAB 

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

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

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: 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
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 = command[2];
    removePercents( friendlyName );
  }
  else
  {
    // get name from contact
    friendlyName   = contact->getFriendlyName();
    contactPicture = contact->getContactPicturePath();
  }


  if( contentType == "text/plain" )
  {
    // This is a regular text message to the user
    QFont   font;
    QString color;

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Message is:" << endl;
    message.print();
#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, 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)
      kdWarning() << "SwitchBoard: Unable to handle P2P message, "
                  << "message values are empty, switchboard noticed an error in a p2p message." << endl;
      return;
    }
    else if(p2pDest != currentAccount_->getHandle())
    {
      // Ignore messages ment for other contacts
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      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_GENERAL
    kdDebug() << "Switchboard: Keep alive message from " << contactHandle << "." << endl;
#endif
  }
  else
  {
    kdDebug() << "SwitchBoard got unhandled message type (type=" << contentType << " contact=" << contactHandle << ")." << endl;
  }
}



// Parse a payload command
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;
  }
}



// Send a message to the contact(s)
void MsnSwitchboardConnection::sendChatMessage(QString text)
{
  if ( currentAccount_ == 0 )
  {
    kdWarning() << "MsnSwitchboardConnection::sendChatMessage - currentAccount_ is null!" << endl;
    return;
  }

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

  // Convert data
  addPercents( 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);
}



// 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 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
#ifdef KMESSTEST
    ASSERT( connectionState_ != SB_CHAT_STARTED );
#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
      {
#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
        // TODO: test what happens in a multi-chat, when isExclusiveChatWithContact() fails
        // TODO: remove this signal, initialize a whole new switchboard connection for the chat window.
        emit requestChat( 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);
}



// 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 chat
void MsnSwitchboardConnection::startChat(const ChatInformation &chatInfo)
{
#ifdef KMESSTEST
  ASSERT( currentAccount_ != 0 );
#endif

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


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

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Contact resumed the chat but uses a different server, reconnecting." << endl;
#endif
    closeConnection();
  }

  // TODO: abort applications when starting new chat?

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - Connect to " << chatInfo.getIp() << ":" << chatInfo.getPort() << "." << endl;
#endif

  // Store information from the chatinfo object
  authorization_   = chatInfo.getAuthorization();
  chatId_          = chatInfo.getChatId();
  userStartedChat_ = chatInfo.getUserStartedChat();
  firstContact_    = chatInfo.getContactHandle();
  lastContact_     = QString::null;

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

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

  connectionState_   = SB_CONNECTING;
  closingConnection_ = false;
}



// Start an offline chat
void MsnSwitchboardConnection::startOfflineChat(const QString &handle)
{
#ifdef KMESSTEST
  ASSERT( connectionState_ == SB_DISCONNECTED );
#endif

  // Set lastContact_ so isExclusiveChatWithContact() returns true,
  // otherwise a chat windows would be spawned for every offline message.
  lastContact_       = handle;
  closingConnection_ = 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 );
}



// 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_GENERAL
  kdDebug() << "Switchboard: Stored message for acknowledgement. "
            << "There are currently " << unAckedMessages_.count() << " messages kept, "
            << acksPending_ << " need to be ACKed." << endl;
#endif
}



#include "msnswitchboardconnection.moc"

Generated by  Doxygen 1.6.0   Back to index