You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdenetwork/kopete/protocols/jabber/jabberbasecontact.cpp

677 lines
21 KiB

/*
* jabbercontact.cpp - Base class for the Kopete Jabber protocol contact
*
* Copyright (c) 2002-2004 by Till Gerken <till@tantalo.net>
* Copyright (c) 2006 by Olivier Goffart <ogoffart at kde.org>
*
* Kopete (c) by the Kopete developers <kopete-devel@kde.org>
*
* *************************************************************************
* * *
* * 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 <kdebug.h>
#include <klocale.h>
#include <kiconloader.h>
#include <kstandarddirs.h>
#include <qtimer.h>
#include <qimage.h>
#include <qregexp.h>
#include <kmessagebox.h>
#include <kio/netaccess.h>
#include <kopetegroup.h>
#include <kopetecontactlist.h>
#include "jabberbasecontact.h"
#include "xmpp_tasks.h"
#include "jabberprotocol.h"
#include "jabberaccount.h"
#include "jabberresource.h"
#include "jabberresourcepool.h"
#include "kopetemetacontact.h"
#include "kopetemessage.h"
#include "kopeteuiglobal.h"
#include "jabbertransport.h"
#include "dlgjabbervcard.h"
/**
* JabberBaseContact constructor
*/
JabberBaseContact::JabberBaseContact (const XMPP::RosterItem &rosterItem, Kopete::Account *account, Kopete::MetaContact * mc, const QString &legacyId)
: Kopete::Contact (account, legacyId.isEmpty() ? rosterItem.jid().full() : legacyId , mc )
{
setDontSync ( false );
JabberTransport *t=transport();
m_account= t ? t->account() : static_cast<JabberAccount *>(Kopete::Contact::account());
// take roster item and update display name
updateContact ( rosterItem );
}
JabberProtocol *JabberBaseContact::protocol ()
{
return static_cast<JabberProtocol *>(Kopete::Contact::protocol ());
}
JabberTransport * JabberBaseContact::transport( )
{
return dynamic_cast<JabberTransport*>(Kopete::Contact::account());
}
/* Return if we are reachable (defaults to true because
we can send on- and offline, only return false if the
account itself is offline, too) */
bool JabberBaseContact::isReachable ()
{
if (account()->isConnected())
return true;
return false;
}
void JabberBaseContact::updateContact ( const XMPP::RosterItem & item )
{
kdDebug ( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Synchronizing local copy of " << contactId() << " with information received from server. (name='" << item.name() << "' groups='" << item.groups() << "')"<< endl;
mRosterItem = item;
// if we don't have a meta contact yet, stop processing here
if ( !metaContact () )
return;
/*
* We received the information from the server, as such,
* don't attempt to synch while we update our local copy.
*/
setDontSync ( true );
// The myself contact is not in the roster on server, ignore this code
// because the myself MetaContact displayname become the latest
// Jabber acccount jid.
if( metaContact() != Kopete::ContactList::self()->myself() )
{
// only update the alias if its not empty
if ( !item.name().isEmpty () && item.name() != item.jid().bare() )
{
QString newName = item.name ();
QString oldName = metaContact()->displayName();
Kopete::Contact *source=metaContact()->displayNameSourceContact();
// kdDebug ( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "setting display name of " << contactId () << " to " << newName << endl;
metaContact()->setDisplayName ( newName );
//automatically set to custom source if the source is to this contact.
if( metaContact()->displayNameSource()==Kopete::MetaContact::SourceContact && newName != oldName && ( source == this || source == 0L ) )
metaContact()->setDisplayNameSource( Kopete::MetaContact::SourceCustom );
}
}
/*
* Set the contact's subscription status
*/
switch ( item.subscription().type () )
{
case XMPP::Subscription::None:
setProperty ( protocol()->propSubscriptionStatus,
i18n ( "You cannot see each others' status." ) );
break;
case XMPP::Subscription::To:
setProperty ( protocol()->propSubscriptionStatus,
i18n ( "You can see this contact's status but they cannot see your status." ) );
break;
case XMPP::Subscription::From:
setProperty ( protocol()->propSubscriptionStatus,
i18n ( "This contact can see your status but you cannot see their status." ) );
break;
case XMPP::Subscription::Both:
setProperty ( protocol()->propSubscriptionStatus,
i18n ( "You can see each others' status." ) );
break;
}
if( !metaContact()->isTemporary() )
{
/*
* In this method, as opposed to KC::syncGroups(),
* the group list from the server is authoritative.
* As such, we need to find a list of all groups
* that the meta contact resides in but does not
* reside in on the server anymore, as well as all
* groups that the meta contact does not reside in,
* but resides in on the server.
* Then, we'll have to synchronize the KMC using
* that information.
*/
Kopete::GroupList groupsToRemoveFrom, groupsToAddTo;
// find all groups our contact is in but that are not in the server side roster
for ( unsigned i = 0; i < metaContact()->groups().count (); i++ )
{
if ( item.groups().find ( metaContact()->groups().at(i)->displayName () ) == item.groups().end () )
groupsToRemoveFrom.append ( metaContact()->groups().at ( i ) );
}
// now find all groups that are in the server side roster but not in the local group
for ( unsigned i = 0; i < item.groups().count (); i++ )
{
bool found = false;
for ( unsigned j = 0; j < metaContact()->groups().count (); j++)
{
if ( metaContact()->groups().at(j)->displayName () == *item.groups().at(i) )
{
found = true;
break;
}
}
if ( !found )
{
groupsToAddTo.append ( Kopete::ContactList::self()->findGroup ( *item.groups().at(i) ) );
}
}
/*
* Special case: if we don't add the contact to any group and the
* list of groups to remove from contains the top level group, we
* risk removing the contact from the visible contact list. In this
* case, we need to make sure at least the top level group stays.
*/
if ( ( groupsToAddTo.count () == 0 ) && ( groupsToRemoveFrom.contains ( Kopete::Group::topLevel () ) ) )
{
groupsToRemoveFrom.remove ( Kopete::Group::topLevel () );
}
for ( Kopete::Group *group = groupsToRemoveFrom.first (); group; group = groupsToRemoveFrom.next () )
{
kdDebug ( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Removing " << contactId() << " from group " << group->displayName () << endl;
metaContact()->removeFromGroup ( group );
}
for ( Kopete::Group *group = groupsToAddTo.first (); group; group = groupsToAddTo.next () )
{
kdDebug ( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Adding " << contactId() << " to group " << group->displayName () << endl;
metaContact()->addToGroup ( group );
}
}
/*
* Enable updates for the server again.
*/
setDontSync ( false );
//can't do it now because it's called from contructor at a point some virtual function are not available
QTimer::singleShot(0, this, SLOT(reevaluateStatus()));
}
void JabberBaseContact::updateResourceList ()
{
/*
* Set available resources.
* This is a bit more complicated: We need to generate
* all images dynamically from the KOS icons and store
* them into the mime factory, then plug them into
* the richtext.
*/
JabberResourcePool::ResourceList resourceList;
account()->resourcePool()->findResources ( rosterItem().jid() , resourceList );
if ( resourceList.isEmpty () )
{
removeProperty ( protocol()->propAvailableResources );
return;
}
QString resourceListStr = "<table cellspacing=\"0\">";
for ( JabberResourcePool::ResourceList::iterator it = resourceList.begin (); it != resourceList.end (); ++it )
{
// icon, resource name and priority
resourceListStr += QString ( "<tr><td><img src=\"kopete-onlinestatus-icon:%1\" /> <b>%2</b> (Priority: %3)</td></tr>" ).
arg ( protocol()->resourceToKOS((*it)->resource()).mimeSourceFor ( account () ),
(*it)->resource().name (), QString::number ( (*it)->resource().priority () ) );
// client name, version, OS
if ( !(*it)->clientName().isEmpty () )
{
resourceListStr += QString ( "<tr><td>%1: %2 (%3)</td></tr>" ).
arg ( i18n ( "Client" ), (*it)->clientName (), (*it)->clientSystem () );
}
// Supported features
#if 0 //disabled because it's just an ugly and long list of incomprehensible namespaces to the user
QStringList supportedFeatures = (*it)->features().list();
QStringList::ConstIterator featuresIt, featuresItEnd = supportedFeatures.constEnd();
if( !supportedFeatures.empty() )
resourceListStr += QString( "<tr><td>Supported Features:" );
for( featuresIt = supportedFeatures.constBegin(); featuresIt != featuresItEnd; ++featuresIt )
{
XMPP::Features tempFeature(*featuresIt);
resourceListStr += QString("\n<br>");
if ( tempFeature.id() > XMPP::Features::FID_None )
resourceListStr += tempFeature.name() + QString(" (");
resourceListStr += *featuresIt;
if ( tempFeature.id() > Features::FID_None )
resourceListStr += QString(")");
}
if( !supportedFeatures.empty() )
resourceListStr += QString( "</td></tr>" );
#endif
// resource timestamp
resourceListStr += QString ( "<tr><td>%1: %2</td></tr>" ).
arg ( i18n ( "Timestamp" ), KGlobal::locale()->formatDateTime ( (*it)->resource().status().timeStamp(), true, true ) );
// message, if any
if ( !(*it)->resource().status().status().stripWhiteSpace().isEmpty () )
{
resourceListStr += QString ( "<tr><td>%1: %2</td></tr>" ).
arg (
i18n ( "Message" ),
Kopete::Message::escape( (*it)->resource().status().status () )
);
}
}
resourceListStr += "</table>";
setProperty ( protocol()->propAvailableResources, resourceListStr );
}
void JabberBaseContact::reevaluateStatus ()
{
kdDebug (JABBER_DEBUG_GLOBAL) << k_funcinfo << "Determining new status for " << contactId () << endl;
Kopete::OnlineStatus status;
XMPP::Resource resource = account()->resourcePool()->bestResource ( mRosterItem.jid () );
status = protocol()->resourceToKOS ( resource );
/* Add some icon to show the subscription */
if( ( mRosterItem.subscription().type() == XMPP::Subscription::None || mRosterItem.subscription().type() == XMPP::Subscription::From)
&& inherits ( "JabberContact" ) && metaContact() != Kopete::ContactList::self()->myself() && account()->isConnected() )
{
status = Kopete::OnlineStatus(status.status() ,
status.weight() ,
protocol() ,
status.internalStatus() | 0x0100,
status.overlayIcons() + QStringList("status_unknown_overlay") , //FIXME: find better icon
status.description() );
}
updateResourceList ();
kdDebug (JABBER_DEBUG_GLOBAL) << k_funcinfo << "New status for " << contactId () << " is " << status.description () << endl;
setOnlineStatus ( status );
/*
* Set away message property.
* We just need to read it from the current resource.
*/
if ( !resource.status ().status ().isEmpty () )
{
setProperty ( protocol()->propAwayMessage, resource.status().status () );
}
else
{
removeProperty ( protocol()->propAwayMessage );
}
}
QString JabberBaseContact::fullAddress ()
{
XMPP::Jid jid = rosterItem().jid();
if ( jid.resource().isEmpty () )
{
jid.setResource ( account()->resourcePool()->bestResource ( jid ).name () );
}
return jid.full ();
}
XMPP::Jid JabberBaseContact::bestAddress ()
{
// see if we are subscribed with a preselected resource
if ( !mRosterItem.jid().resource().isEmpty () )
{
// we have a preselected resource, so return our default full address
return mRosterItem.jid ();
}
// construct address out of user@host and current best resource
XMPP::Jid jid = mRosterItem.jid ();
jid.setResource ( account()->resourcePool()->bestResource( mRosterItem.jid() ).name () );
return jid;
}
void JabberBaseContact::setDontSync ( bool flag )
{
mDontSync = flag;
}
bool JabberBaseContact::dontSync ()
{
return mDontSync;
}
void JabberBaseContact::serialize (QMap < QString, QString > &serializedData, QMap < QString, QString > & /* addressBookData */ )
{
// Contact id and display name are already set for us, only add the rest
serializedData["JID"] = mRosterItem.jid().full();
serializedData["groups"] = mRosterItem.groups ().join (QString::fromLatin1 (","));
}
void JabberBaseContact::slotUserInfo( )
{
if ( !account()->isConnected () )
{
account()->errorConnectFirst ();
return;
}
// Update the vCard
//slotGetTimedVCard();
new dlgJabberVCard ( account(), this, Kopete::UI::Global::mainWidget () );
}
void JabberBaseContact::setPropertiesFromVCard ( const XMPP::VCard &vCard )
{
kdDebug ( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Updating vCard for " << contactId () << endl;
// update vCard cache timestamp if this is not a temporary contact
if ( metaContact() && !metaContact()->isTemporary () )
{
setProperty ( protocol()->propVCardCacheTimeStamp, QDateTime::currentDateTime().toString ( Qt::ISODate ) );
}
/*
* Set the nickname property.
* but ignore it if we are in a groupchat, or it will clash with the normal nickname
*/
if(inherits ( "JabberContact" ))
{
if ( !vCard.nickName().isEmpty () )
{
setProperty ( protocol()->propNickName, vCard.nickName () );
}
else
{
removeProperty ( protocol()->propNickName );
}
}
/**
* Kopete does not allow a modification of the "full name"
* property. However, some vCards specify only the full name,
* some specify only first and last name.
* Due to these inconsistencies, if first and last name don't
* exist, it is attempted to parse the full name.
*/
// remove all properties first
removeProperty ( protocol()->propFirstName );
removeProperty ( protocol()->propLastName );
removeProperty ( protocol()->propFullName );
if ( !vCard.fullName().isEmpty () && vCard.givenName().isEmpty () && vCard.familyName().isEmpty () )
{
QString lastName = vCard.fullName().section ( ' ', 0, -1 );
QString firstName = vCard.fullName().left(vCard.fullName().length () - lastName.length ()).stripWhiteSpace ();
setProperty ( protocol()->propFirstName, firstName );
setProperty ( protocol()->propLastName, lastName );
}
else
{
if ( !vCard.givenName().isEmpty () )
setProperty ( protocol()->propFirstName, vCard.givenName () );
if ( !vCard.familyName().isEmpty () )
setProperty ( protocol()->propLastName, vCard.familyName () );
}
if( !vCard.fullName().isEmpty() )
setProperty ( protocol()->propFullName, vCard.fullName() );
/*
* Set the general information
*/
removeProperty( protocol()->propJid );
removeProperty( protocol()->propBirthday );
removeProperty( protocol()->propTimezone );
removeProperty( protocol()->propHomepage );
setProperty( protocol()->propJid, vCard.jid() );
if( !vCard.bdayStr().isEmpty () )
setProperty( protocol()->propBirthday, vCard.bdayStr() );
if( !vCard.timezone().isEmpty () )
setProperty( protocol()->propTimezone, vCard.timezone() );
if( !vCard.url().isEmpty () )
setProperty( protocol()->propHomepage, vCard.url() );
/*
* Set the work information.
*/
removeProperty( protocol()->propCompanyName );
removeProperty( protocol()->propCompanyDepartement );
removeProperty( protocol()->propCompanyPosition );
removeProperty( protocol()->propCompanyRole );
if( !vCard.org().name.isEmpty() )
setProperty( protocol()->propCompanyName, vCard.org().name );
if( !vCard.org().unit.join(",").isEmpty() )
setProperty( protocol()->propCompanyDepartement, vCard.org().unit.join(",")) ;
if( !vCard.title().isEmpty() )
setProperty( protocol()->propCompanyPosition, vCard.title() );
if( !vCard.role().isEmpty() )
setProperty( protocol()->propCompanyRole, vCard.role() );
/*
* Set the about information
*/
removeProperty( protocol()->propAbout );
if( !vCard.desc().isEmpty() )
setProperty( protocol()->propAbout, vCard.desc() );
/*
* Set the work and home addresses information
*/
removeProperty( protocol()->propWorkStreet );
removeProperty( protocol()->propWorkExtAddr );
removeProperty( protocol()->propWorkPOBox );
removeProperty( protocol()->propWorkCity );
removeProperty( protocol()->propWorkPostalCode );
removeProperty( protocol()->propWorkCountry );
removeProperty( protocol()->propHomeStreet );
removeProperty( protocol()->propHomeExtAddr );
removeProperty( protocol()->propHomePOBox );
removeProperty( protocol()->propHomeCity );
removeProperty( protocol()->propHomePostalCode );
removeProperty( protocol()->propHomeCountry );
for(XMPP::VCard::AddressList::const_iterator it = vCard.addressList().begin(); it != vCard.addressList().end(); it++)
{
XMPP::VCard::Address address = (*it);
if(address.work)
{
setProperty( protocol()->propWorkStreet, address.street );
setProperty( protocol()->propWorkExtAddr, address.extaddr );
setProperty( protocol()->propWorkPOBox, address.pobox );
setProperty( protocol()->propWorkCity, address.locality );
setProperty( protocol()->propWorkPostalCode, address.pcode );
setProperty( protocol()->propWorkCountry, address.country );
}
else
if(address.home)
{
setProperty( protocol()->propHomeStreet, address.street );
setProperty( protocol()->propHomeExtAddr, address.extaddr );
setProperty( protocol()->propHomePOBox, address.pobox );
setProperty( protocol()->propHomeCity, address.locality );
setProperty( protocol()->propHomePostalCode, address.pcode );
setProperty( protocol()->propHomeCountry, address.country );
}
}
/*
* Delete emails first, they might not be present
* in the vCard at all anymore.
*/
removeProperty ( protocol()->propEmailAddress );
removeProperty ( protocol()->propWorkEmailAddress );
/*
* Set the home and work email information.
*/
XMPP::VCard::EmailList::const_iterator emailEnd = vCard.emailList().end ();
for(XMPP::VCard::EmailList::const_iterator it = vCard.emailList().begin(); it != emailEnd; ++it)
{
XMPP::VCard::Email email = (*it);
if(email.work)
{
if( !email.userid.isEmpty() )
setProperty ( protocol()->propWorkEmailAddress, email.userid );
}
else
if(email.home)
{
if( !email.userid.isEmpty() )
setProperty ( protocol()->propEmailAddress, email.userid );
}
}
/*
* Delete phone number properties first as they might have
* been unset during an update and are not present in
* the vCard at all anymore.
*/
removeProperty ( protocol()->propPrivatePhone );
removeProperty ( protocol()->propPrivateMobilePhone );
removeProperty ( protocol()->propWorkPhone );
removeProperty ( protocol()->propWorkMobilePhone );
/*
* Set phone numbers. Note that if a mobile phone number
* is specified, it's assigned to the private mobile
* phone number property. This might not be the desired
* behavior for all users.
*/
XMPP::VCard::PhoneList::const_iterator phoneEnd = vCard.phoneList().end ();
for(XMPP::VCard::PhoneList::const_iterator it = vCard.phoneList().begin(); it != phoneEnd; ++it)
{
XMPP::VCard::Phone phone = (*it);
if(phone.work)
{
setProperty ( protocol()->propWorkPhone, phone.number );
}
else
if(phone.fax)
{
setProperty ( protocol()->propPhoneFax, phone.number);
}
else
if(phone.cell)
{
setProperty ( protocol()->propPrivateMobilePhone, phone.number );
}
else
if(phone.home)
{
setProperty ( protocol()->propPrivatePhone, phone.number );
}
}
/*
* Set photo/avatar property.
*/
removeProperty( protocol()->propPhoto );
QImage contactPhoto;
QString fullJid = mRosterItem.jid().full();
QString finalPhotoPath = locateLocal("appdata", "jabberphotos/" + fullJid.replace(QRegExp("[./~]"),"-") +".png");
// photo() is a QByteArray
if ( !vCard.photo().isEmpty() )
{
kdDebug( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Contact has a photo embedded into his vCard." << endl;
// QImage is used to save to disk in PNG later.
contactPhoto = QImage( vCard.photo() );
}
// Contact photo is a URI.
else if( !vCard.photoURI().isEmpty() )
{
QString tempPhotoPath = 0;
// Downalod photo from URI.
if( !KIO::NetAccess::download( vCard.photoURI(), tempPhotoPath, 0) )
{
KMessageBox::queuedMessageBox( Kopete::UI::Global::mainWidget (), KMessageBox::Sorry, i18n( "Downloading of Jabber contact photo failed!" ) );
return;
}
kdDebug( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Contact photo is a URI." << endl;
contactPhoto = QImage( tempPhotoPath );
KIO::NetAccess::removeTempFile( tempPhotoPath );
}
// Save the image to the disk, then set the property.
if( !contactPhoto.isNull() && contactPhoto.save(finalPhotoPath, "PNG") )
{
kdDebug( JABBER_DEBUG_GLOBAL ) << k_funcinfo << "Setting photo for contact: " << fullJid << endl;
setProperty( protocol()->propPhoto, finalPhotoPath );
}
}
#include "jabberbasecontact.moc"
// vim: set noet ts=4 sts=4 sw=4: