/* * psicontact.cpp - PsiContact * Copyright (C) 2008 Yandex LLC (Michail Pishchagin) * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "psicontact.h" #include #include "avatars.h" #include "common.h" #include "contactview.h" #include "iconset.h" #include "jidutil.h" #include "profiles.h" #include "psiaccount.h" #include "psicontactmenu.h" #include "psiiconset.h" #include "psioptions.h" #include "resourcemenu.h" #include "userlist.h" #include "userlist.h" #include "alertable.h" #include "avatars.h" #ifdef YAPSI #include "yaprivacymanager.h" #include "yacommon.h" #include "yaprofile.h" #include "yatoastercentral.h" #endif #include "contactlistgroup.h" #include "desktoputil.h" #include "yaremoveconfirmationmessagebox.h" #include "vcardfactory.h" #include "psicon.h" #include "psicontactlist.h" #include "yarostertooltip.h" #include "psilogger.h" static const int statusTimerInterval = 5000; class PsiContact::Private : public Alertable { Q_OBJECT public: Private(PsiContact* contact) : account_(0) , statusTimer_(0) , gender_(XMPP::VCard::UnknownGender) , genderCached_(false) , fake_(false) , contact_(contact) { oldStatus_ = XMPP::Status(XMPP::Status::Offline); #ifdef YAPSI showOnlineTemporarily_ = false; reconnecting_ = false; delayedMoodUpdateTimer_ = new QTimer(this); delayedMoodUpdateTimer_->setInterval(60 * 1000); delayedMoodUpdateTimer_->setSingleShot(true); connect(delayedMoodUpdateTimer_, SIGNAL(timeout()), contact, SLOT(moodUpdate())); #endif statusTimer_ = new QTimer(this); statusTimer_->setInterval(statusTimerInterval); statusTimer_->setSingleShot(true); connect(statusTimer_, SIGNAL(timeout()), SLOT(updateStatus())); } ~Private() { } PsiAccount* account_; QTimer* statusTimer_; UserListItem u_; QString name_; Status status_; Status oldStatus_; #ifdef YAPSI bool showOnlineTemporarily_; bool reconnecting_; QTimer* delayedMoodUpdateTimer_; QMap resourceMood_; #endif XMPP::VCard::Gender gender_; bool genderCached_; QString comparisonName_; bool fake_; XMPP::Status status(const UserListItem& u) const { XMPP::Status status = XMPP::Status(XMPP::Status::Offline); if (u.priority() != u.userResourceList().end()) status = (*u.priority()).status(); return status; } /** * Returns userHost of the base jid combined with \param resource. */ Jid jidForResource(const QString& resource) { QString s = u_.jid().userHost(); if (!resource.isEmpty()) { s += '/'; s += resource; } return Jid(s); } void setStatus(XMPP::Status status) { status_ = status; reconnecting_ = false; if (account_ && !account_->notifyOnline()) oldStatus_ = status_; else statusTimer_->start(); } private slots: void updateStatus() { showOnlineTemporarily_ = false; oldStatus_ = status_; emit contact_->updated(); } private: PsiContact* contact_; void alertFrameUpdated() { #ifndef YAPSI emit contact_->updated(); #endif } }; /** * Creates new PsiContact. */ PsiContact::PsiContact(const UserListItem& u, PsiAccount* account) : ContactListItem(account) { d = new Private(this); d->account_ = account; if (d->account_) { connect(d->account_->avatarFactory(), SIGNAL(avatarChanged(const Jid&)), SLOT(avatarChanged(const Jid&))); } connect(VCardFactory::instance(), SIGNAL(vcardChanged(const Jid&)), SLOT(vcardChanged(const Jid&))); update(u); // updateParent(); } PsiContact::PsiContact() : ContactListItem(0) { d = new Private(this); d->account_ = 0; } /** * Destructor. */ PsiContact::~PsiContact() { emit destroyed(this); delete d; } /** * Returns account to which a contact belongs. */ PsiAccount* PsiContact::account() const { return d->account_; } /** * TODO: Think of ways to remove this function. */ const UserListItem& PsiContact::userListItem() const { return d->u_; } /** * This function should be called only by PsiAccount to update * PsiContact's current state. */ void PsiContact::update(const UserListItem& u) { d->u_ = u; Status status = d->status(d->u_); d->comparisonName_ = QString(name() + jid().full() + account()->name()).lower(); d->setStatus(status); emit updated(); emit groupsChanged(); moodUpdate(); } #ifdef YAPSI void PsiContact::startDelayedMoodUpdate(int timeoutInSecs) { d->delayedMoodUpdateTimer_->setInterval((timeoutInSecs + 1) * 1000); d->delayedMoodUpdateTimer_->start(); } QString PsiContact::getMood(const QString& resource) const { if (!d->resourceMood_.contains(resource)) return QString(); return d->resourceMood_[resource]; } void PsiContact::setMood(const QString& resource, const QString& mood) { d->resourceMood_[resource] = mood; } void PsiContact::moodUpdate() { QStringList currentlyOnlineResources; foreach(const UserResource& r, d->u_.userResourceList()) { currentlyOnlineResources << r.name(); Status status = r.status(); QString oldMood = d->resourceMood_[r.name()]; QString newMood = Ya::processMood(oldMood, status.status(), status.type()); if (status.isAvailable() && // !status.status().isEmpty() && newMood != oldMood && !d->u_.isSelf() && !isYaInformer()) { PsiLogger::instance()->log(QString("mood changed: %1 / %2, '%3' (%4)") .arg(jid().full()).arg(r.name()) .arg(status.status()).arg(oldMood)); emit moodChanged(r.name(), newMood); } } QMutableMapIterator it(d->resourceMood_); while (it.hasNext()) { it.next(); if (!currentlyOnlineResources.contains(it.key())) { PsiLogger::instance()->log(QString("mood removing resource %1 / %2") .arg(jid().full()).arg(it.key())); it.remove(); } } } #endif /** * Triggers default action. */ void PsiContact::activate() { if (!account()) return; // FIXME: probably should obsolete this option // if(option.singleclick) // return; account()->actionDefault(jid()); } ContactListModel::Type PsiContact::type() const { return ContactListModel::ContactType; } /** * Returns contact's display name. */ const QString& PsiContact::name() const { d->name_ = JIDUtil::nickOrJid(d->u_.name(), jid().full()); return d->name_; } const QString& PsiContact::comparisonName() const { return d->comparisonName_; } XMPP::VCard::Gender PsiContact::gender() const { if (!d->genderCached_) const_cast(this)->vcardChanged(jid()); return d->gender_; } /** * Returns contact's Jabber ID. */ const XMPP::Jid& PsiContact::jid() const { return d->u_.jid(); } /** * Returns contact's status. */ Status PsiContact::status() const { #ifdef YAPSI if (isBlocked()) { return XMPP::Status(XMPP::Status::Blocked); } if (d->reconnecting_) { return XMPP::Status(XMPP::Status::Reconnecting); } if (!authorizesToSeeStatus()) { return XMPP::Status(XMPP::Status::NotAuthorizedToSeeStatus); } #endif return d->status_; } /** * Returns tool tip text for contact in HTML format. */ QString PsiContact::toolTip() const { return d->u_.makeTip(true, false); } /** * Returns contact's avatar picture. */ QIcon PsiContact::picture() const { if (!account()) return QIcon(); return account()->avatarFactory()->getAvatar(jid().bare()); } /** * Returns contact's status picture, or alert frame, if alert is present. */ QIcon PsiContact::statusIcon() const { if (d->alerting()) return d->currentAlertFrame(); return PsiIconset::instance()->status(jid(), status()).icon(); } /** * TODO */ ContactListItemMenu* PsiContact::contextMenu(ContactListModel* model) { if (!account()) return 0; return new PsiContactMenu(this, model); } bool PsiContact::isFake() const { return d->fake_; } void PsiContact::setFake(bool fake) { d->fake_ = fake; } /** * Returns true if user could modify (i.e. rename/change group/remove from * contact list) this contact on server. */ bool PsiContact::isEditable() const { if (!account()) return false; #ifdef YAPSI_ACTIVEX_SERVER if (account()->psi() && account()->psi()->contactList() && account()->psi()->contactList()->onlineAccount() && !account()->psi()->contactList()->onlineAccount()->isAvailable()) { return false; } #endif return account()->isAvailable(); } bool PsiContact::isRenameable() const { return isEditable() && inList(); } bool PsiContact::isDragEnabled() const { return isEditable() && !d->u_.isPrivate() // && !isAgent() ; } /** * This function should be invoked when contact is being renamed by * user. \param name specifies new name. PsiContact is responsible * for the actual roster update. */ void PsiContact::setName(const QString& name) { if (account()) account()->actionRename(jid(), name); } QString PsiContact::generalGroupName() { return tr("General"); } QString PsiContact::notInListGroupName() { return tr("Not in list"); } QString PsiContact::privateMessagesGroupName() { return tr("Private messages"); } static const QString globalGroupDelimiter = "::"; static const QString accountGroupDelimiter = "::"; /** * Processes the list of groups to the result ready to be fed into * ContactListModel. All account-specific group delimiters are replaced * in favor of the global Psi one. */ QStringList PsiContact::groups() const { QStringList result; if (!account()) return result; if (d->u_.isPrivate()) { result << privateMessagesGroupName(); return result; } if (!d->u_.inList() && !isFake()) { result << notInListGroupName(); return result; } if (d->u_.groups().isEmpty()) { // empty group name means that the contact should be added // to the 'General' group or no group at all #ifdef USE_GENERAL_CONTACT_GROUP result << generalGroupName(); #else result << QString(); #endif } else { foreach(QString group, d->u_.groups()) { QString groupName = group.split(accountGroupDelimiter).join(globalGroupDelimiter); #ifdef USE_GENERAL_CONTACT_GROUP if (groupName.isEmpty()) { groupName = generalGroupName(); } #endif result << groupName; } } return result; } /** * Sets contact's groups to be \param newGroups. All associations to all groups * it belonged to prior to calling this function are lost. \param newGroups * becomes the only groups of the contact. Note: \param newGroups should be passed * with global Psi group delimiters. */ void PsiContact::setGroups(QStringList newGroups) { if (!account()) return; QStringList newAccountGroups; foreach(QString group, newGroups) { QString groupName = group.split(globalGroupDelimiter).join(accountGroupDelimiter); #ifdef USE_GENERAL_CONTACT_GROUP if (groupName == generalGroupName()) { groupName = QString(); } #endif newAccountGroups << groupName; } if (newAccountGroups.count() == 1 && newAccountGroups.first().isEmpty()) newAccountGroups.clear(); account()->actionGroupsSet(jid(), newAccountGroups); } bool PsiContact::groupOperationPermitted(const QString& oldGroupName, const QString& newGroupName) const { Q_UNUSED(oldGroupName); Q_UNUSED(newGroupName); #ifdef YAPSI // FIXME: make sure YaRoster::addGroup() and YaRoster::removeContact() operate // on the same model. And that that model is YaRoster::contactsModel_. if (!account()) return false; #endif return true; } bool PsiContact::isRemovable() const { foreach(QString group, groups()) { if (!groupOperationPermitted(group, QString())) return false; } #ifdef YAPSI_ACTIVEX_SERVER if (account()->psi() && account()->psi()->contactList() && account()->psi()->contactList()->onlineAccount() && !account()->psi()->contactList()->onlineAccount()->isAvailable()) { return false; } #endif return true; } /** * Returns true if contact currently have an alert set. */ bool PsiContact::alerting() const { return d->alerting(); } /** * Sets alert icon for contact. Pass null pointer in order to clear alert. */ void PsiContact::setAlert(const PsiIcon* icon) { d->setAlert(icon); // updateParent(); } /** * Contact should always be visible if it's alerting. */ bool PsiContact::shouldBeVisible() const { #ifndef YAPSI if (d->alerting()) return true; #endif return false; // return ContactListItem::shouldBeVisible(); } /** * Standard desired parent. */ // ContactListGroupItem* PsiContact::desiredParent() const // { // return ContactListContact::desiredParent(); // } /** * Returns true if this contact is the one for the \param jid. In case * if reimplemented in metacontact-enabled subclass, it could match * several different jids. */ bool PsiContact::find(const Jid& jid) const { return this->jid().compare(jid); } /** * This behaves just like ContactListContact::contactList(), but statically * casts returned value to PsiContactList. */ // PsiContactList* PsiContact::contactList() const // { // return static_cast(ContactListContact::contactList()); // } /** * Creates ResourceMenu to select from all online resources for a contact. */ ResourceMenu* PsiContact::createResourceMenu(QWidget* parent) const { if (!account()) return 0; ResourceMenu* menu = new ResourceMenu(parent); foreach(UserResource resource, d->u_.userResourceList()) menu->addResource(resource); return menu; } /** * Creates ResourceMenu to select a resource from the list of currently * active resources (i.e. with which conversations do currently exist). */ ResourceMenu* PsiContact::createActiveChatsMenu(QWidget* parent) const { if (!account()) return 0; ResourceMenu* menu = new ResourceMenu(parent); foreach(QString resourceName, account()->hiddenChats(jid())) { // determine status int status; const UserResourceList &rl = d->u_.userResourceList(); UserResourceList::ConstIterator uit = rl.find(resourceName); if (uit != rl.end() || (uit = rl.priority()) != rl.end()) status = makeSTATUS((*uit).status()); else status = STATUS_OFFLINE; menu->addResource(status, resourceName); } return menu; } void PsiContact::receiveIncomingEvent() { if (account()) account()->actionRecvEvent(jid()); } void PsiContact::sendMessage() { if (account()) account()->actionSendMessage(jid()); } void PsiContact::sendMessageTo(QString resource) { if (account()) account()->actionSendMessage(d->jidForResource(resource)); } void PsiContact::openChat() { if (account()) account()->actionOpenChat(jid()); } void PsiContact::openChatTo(QString resource) { if (account()) account()->actionOpenChatSpecific(d->jidForResource(resource)); } #ifdef WHITEBOARDING void PsiContact::openWhiteboard() { if (account()) account()->actionOpenWhiteboard(jid()); } void PsiContact::openWhiteboardTo(QString resource) { if (account()) account()->actionOpenWhiteboardSpecific(d->jidForResource(resource)); } #endif void PsiContact::executeCommand(QString resource) { if (account()) account()->actionExecuteCommandSpecific(d->jidForResource(resource), ""); } void PsiContact::openActiveChat(QString resource) { if (account()) account()->actionOpenChatSpecific(d->jidForResource(resource)); } void PsiContact::sendFile() { if (account()) account()->actionSendFile(jid()); } void PsiContact::inviteToGroupchat(QString groupChat) { if (account()) account()->actionInvite(jid(), groupChat); } // TODO: implement PsiAgent class and move these functions there void PsiContact::logon() { qWarning("PsiContact::logon()"); } void PsiContact::logoff() { qWarning("PsiContact::logoff()"); } void PsiContact::toggleBlockedState() { if (!account()) return; YaPrivacyManager* privacyManager = dynamic_cast(account()->privacyManager()); Q_ASSERT(privacyManager); bool blocked = privacyManager->isContactBlocked(jid()); if (!blocked) { emit YaRosterToolTip::instance()->blockContact(this, 0); } else { emit YaRosterToolTip::instance()->unblockContact(this, 0); } } void PsiContact::toggleBlockedStateConfirmation() { if (!account()) return; YaPrivacyManager* privacyManager = dynamic_cast(account()->privacyManager()); Q_ASSERT(privacyManager); bool blocked = privacyManager->isContactBlocked(jid()); blockContactConfirmationHelper(!blocked); } void PsiContact::blockContactConfirmationHelper(bool block) { if (!account()) return; YaPrivacyManager* privacyManager = dynamic_cast(account()->privacyManager()); Q_ASSERT(privacyManager); privacyManager->setContactBlocked(jid(), block); } void PsiContact::blockContactConfirmation(const QString& id, bool confirmed) { Q_ASSERT(id == "blockContact"); if (confirmed) { blockContactConfirmationHelper(true); } } /*! * The only way to get dual authorization with XMPP1.0-compliant servers * is to request auth first and make a contact accept it, and request it * on its own. */ void PsiContact::rerequestAuthorizationFrom() { if (account()) account()->dj_authReq(jid()); } void PsiContact::removeAuthorizationFrom() { qWarning("PsiContact::removeAuthorizationFrom()"); } void PsiContact::remove() { if (account()) account()->actionRemove(jid()); } void PsiContact::assignCustomPicture() { if (!account()) return; // FIXME: Should check the supported filetypes dynamically // FIXME: parent of QFileDialog is NULL, probably should make it psi->contactList() QString file = QFileDialog::getOpenFileName(0, tr("Choose an image"), "", tr("All files (*.png *.jpg *.gif)")); if (!file.isNull()) { account()->avatarFactory()->importManualAvatar(jid(), file); } } void PsiContact::clearCustomPicture() { if (account()) account()->avatarFactory()->removeManualAvatar(jid()); } void PsiContact::userInfo() { if (account()) account()->actionInfo(jid()); } void PsiContact::history() { #ifdef YAPSI if (account()) Ya::showHistory(account(), jid()); #else if (account()) account()->actionHistory(jid()); #endif } #ifdef YAPSI bool PsiContact::isYaInformer() const { return d->u_.groups().contains(Ya::INFORMERS_GROUP_NAME); } bool PsiContact::isYaJid() { return Ya::isYaJid(jid().full()); } bool PsiContact::isYandexTeamJid() { return Ya::isYandexTeamJid(jid().full()); } YaProfile PsiContact::getYaProfile() const { return YaProfile(account(), jid()); } void PsiContact::yaProfile() { if (!account() || !(isYaJid() || isYandexTeamJid())) return; getYaProfile().browse(); } void PsiContact::yaPhotos() { if (!account() || !isYaJid()) return; getYaProfile().browsePhotos(); } void PsiContact::yaEmail() { // FIXME: use vcard.email()? DesktopUtil::openEMail(jid().bare()); } void PsiContact::addRemoveAuthBlockAvailable(bool* addButton, bool* deleteButton, bool* authButton, bool* blockButton) const { Q_ASSERT(addButton && deleteButton && authButton && blockButton); if (!addButton || !deleteButton || !authButton || !blockButton) return; *addButton = false; *deleteButton = false; *authButton = false; *blockButton = false; if (account() && account()->isAvailable() && !userListItem().isSelf()) { UserListItem* u = account()->findFirstRelevant(jid()); *blockButton = isEditable(); *deleteButton = isEditable(); if (!u || !u->inList()) { *addButton = isEditable(); } else { if (!authorizesToSeeStatus()) { *authButton = isEditable(); } } *deleteButton = *deleteButton && isEditable() && isRemovable(); YaPrivacyManager* privacyManager = dynamic_cast(account()->privacyManager()); Q_ASSERT(privacyManager); *blockButton = *blockButton && privacyManager->isAvailable(); } } bool PsiContact::addAvailable() const { bool addButton, deleteButton, authButton, blockButton; addRemoveAuthBlockAvailable(&addButton, &deleteButton, &authButton, &blockButton); return addButton; } bool PsiContact::removeAvailable() const { bool addButton, deleteButton, authButton, blockButton; addRemoveAuthBlockAvailable(&addButton, &deleteButton, &authButton, &blockButton); return deleteButton && !isFake(); } bool PsiContact::authAvailable() const { bool addButton, deleteButton, authButton, blockButton; addRemoveAuthBlockAvailable(&addButton, &deleteButton, &authButton, &blockButton); return authButton; } bool PsiContact::blockAvailable() const { bool addButton, deleteButton, authButton, blockButton; addRemoveAuthBlockAvailable(&addButton, &deleteButton, &authButton, &blockButton); return blockButton; } bool PsiContact::historyAvailable() const { return Ya::historyAvailable(account(), jid()); } #endif void PsiContact::avatarChanged(const Jid& j) { if (!j.compare(jid(), false)) return; emit updated(); } void PsiContact::rereadVCard() { vcardChanged(jid()); } void PsiContact::vcardChanged(const Jid& j) { if (!j.compare(jid(), false)) return; d->gender_ = XMPP::VCard::UnknownGender; const VCard* vcard = VCardFactory::instance()->vcard(jid()); if (vcard) { d->gender_ = vcard->gender(); } d->genderCached_ = true; emit updated(); } static inline int rankStatus(int status) { switch (status) { case XMPP::Status::FFC: return 0; // YAPSI original: 0; case XMPP::Status::Online: return 0; // YAPSI original: 1; case XMPP::Status::Away: return 1; // YAPSI original: 2; case XMPP::Status::XA: return 1; // YAPSI original: 3; case XMPP::Status::DND: return 0; // YAPSI original: 4; case XMPP::Status::Invisible: return 5; // YAPSI original: 5; default: return 6; } return 0; } bool PsiContact::compare(const ContactListItem* other) const { const ContactListGroup* group = dynamic_cast(other); if (group) { return !group->compare(this); } const PsiContact* contact = dynamic_cast(other); if (contact) { int rank = rankStatus(d->oldStatus_.type()) - rankStatus(contact->d->oldStatus_.type()); if (rank == 0) rank = QString::localeAwareCompare(comparisonName(), contact->comparisonName()); return rank < 0; } return ContactListItem::compare(other); } static YaPrivacyManager* privacyManager(PsiAccount* account) { return dynamic_cast(account->privacyManager()); } bool PsiContact::isBlocked() const { return account() && privacyManager(account()) && privacyManager(account())->isContactBlocked(jid()); } bool PsiContact::inList() const { return userListItem().inList(); } /*! * Returns true if contact could see our status. */ bool PsiContact::authorized() const { return userListItem().subscription().type() == Subscription::Both || userListItem().subscription().type() == Subscription::From; } /*! * Returns true if we could see contact's status. */ bool PsiContact::authorizesToSeeStatus() const { return userListItem().subscription().type() == Subscription::Both || userListItem().subscription().type() == Subscription::To; } bool PsiContact::isOnline() const { return d->status_.type() != XMPP::Status::Offline || d->oldStatus_.type() != XMPP::Status::Offline #ifdef YAPSI || d->showOnlineTemporarily_ || d->reconnecting_; #endif ; } bool PsiContact::isHidden() const { return isFake(); } #ifdef YAPSI void PsiContact::showOnlineTemporarily() { d->showOnlineTemporarily_ = true; d->statusTimer_->start(); emit updated(); } void PsiContact::setReconnectingState(bool reconnecting) { d->reconnecting_ = reconnecting; emit updated(); } #endif void PsiContact::setEditing(bool editing) { if (this->editing() != editing) { ContactListItem::setEditing(editing); emit updated(); } } #ifdef YAPSI bool PsiContact::moodNotificationsEnabled() const { return !account()->psi()->yaToasterCentral()->moodNotificationsDisabled(jid()); } void PsiContact::setMoodNotificationsEnabled(bool enabled) { account()->psi()->yaToasterCentral()->setMoodNotificationsDisabled(jid(), !enabled); } #endif #include "psicontact.moc"