1 /*
2     Copyright (C) 2011  Martin Klapetek <martin.klapetek@gmail.com>
3     Copyright (C) 2011  Dario Freddi <dario.freddi@collabora.com>
4 
5     This library is free software; you can redistribute it and/or
6     modify it under the terms of the GNU Lesser General Public
7     License as published by the Free Software Foundation; either
8     version 2.1 of the License, or (at your option) any later version.
9 
10     This library is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13     Lesser General Public License for more details.
14 
15     You should have received a copy of the GNU Lesser General Public
16     License along with this library; if not, write to the Free Software
17     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19 
20 
21 #include "contact-request-handler.h"
22 #include "ktp_kded_debug.h"
23 
24 #include <KTp/error-dictionary.h>
25 #include <KTp/core.h>
26 #include <KTp/Widgets/contact-info-dialog.h>
27 
28 #include <TelepathyQt/Account>
29 #include <TelepathyQt/AccountManager>
30 #include <TelepathyQt/Connection>
31 #include <TelepathyQt/Contact>
32 #include <TelepathyQt/ContactManager>
33 #include <TelepathyQt/PendingComposite>
34 #include <TelepathyQt/PendingOperation>
35 
36 #include <KStatusNotifierItem>
37 #include <KLocalizedString>
38 
39 #include <QAction>
40 #include <QIcon>
41 #include <QFutureWatcher>
42 #include <QtConcurrentFilter>
43 #include <QMenu>
44 
Q_DECLARE_METATYPE(Tp::ContactPtr)45 Q_DECLARE_METATYPE(Tp::ContactPtr)
46 
47 static bool kde_tp_filter_contacts_by_publication_status(const Tp::ContactPtr &contact)
48 {
49     return contact->publishState() == Tp::Contact::PresenceStateAsk && !contact->isBlocked();
50 }
51 
ContactRequestHandler(QObject * parent)52 ContactRequestHandler::ContactRequestHandler(QObject *parent)
53     : QObject(parent)
54 {
55     connect(KTp::accountManager().data(), SIGNAL(newAccount(Tp::AccountPtr)),
56             this, SLOT(onNewAccountAdded(Tp::AccountPtr)));
57 
58     QList<Tp::AccountPtr> accounts = KTp::accountManager()->allAccounts();
59 
60     Q_FOREACH(const Tp::AccountPtr &account, accounts) {
61         onNewAccountAdded(account);
62     }
63 
64 }
65 
~ContactRequestHandler()66 ContactRequestHandler::~ContactRequestHandler()
67 {
68 
69 }
70 
onNewAccountAdded(const Tp::AccountPtr & account)71 void ContactRequestHandler::onNewAccountAdded(const Tp::AccountPtr &account)
72 {
73     qCWarning(KTP_KDED_MODULE);
74     Q_ASSERT(account->isReady(Tp::Account::FeatureCore));
75 
76     if (account->connection()) {
77         handleNewConnection(account->connection());
78     }
79 
80     connect(account.data(),
81             SIGNAL(connectionChanged(Tp::ConnectionPtr)),
82             this, SLOT(onConnectionChanged(Tp::ConnectionPtr)));
83 }
84 
onConnectionChanged(const Tp::ConnectionPtr & connection)85 void ContactRequestHandler::onConnectionChanged(const Tp::ConnectionPtr &connection)
86 {
87     if (!connection.isNull()) {
88         handleNewConnection(connection);
89     }
90 }
91 
handleNewConnection(const Tp::ConnectionPtr & connection)92 void ContactRequestHandler::handleNewConnection(const Tp::ConnectionPtr &connection)
93 {
94     qCDebug(KTP_KDED_MODULE);
95     connect(connection->contactManager().data(), SIGNAL(presencePublicationRequested(Tp::Contacts)),
96             this, SLOT(onPresencePublicationRequested(Tp::Contacts)));
97 
98     connect(connection->contactManager().data(),
99             SIGNAL(stateChanged(Tp::ContactListState)),
100             this, SLOT(onContactManagerStateChanged(Tp::ContactListState)));
101 
102     onContactManagerStateChanged(connection->contactManager(),
103                                  connection->contactManager()->state());
104 }
105 
onContactManagerStateChanged(Tp::ContactListState state)106 void ContactRequestHandler::onContactManagerStateChanged(Tp::ContactListState state)
107 {
108     onContactManagerStateChanged(Tp::ContactManagerPtr(qobject_cast< Tp::ContactManager* >(sender())), state);
109 }
110 
onContactManagerStateChanged(const Tp::ContactManagerPtr & contactManager,Tp::ContactListState state)111 void ContactRequestHandler::onContactManagerStateChanged(const Tp::ContactManagerPtr &contactManager,
112                                                          Tp::ContactListState state)
113 {
114     if (state == Tp::ContactListStateSuccess) {
115         QFutureWatcher< Tp::ContactPtr > *watcher = new QFutureWatcher< Tp::ContactPtr >(this);
116         connect(watcher, SIGNAL(finished()), this, SLOT(onAccountsPresenceStatusFiltered()));
117         watcher->setFuture(QtConcurrent::filtered(contactManager->allKnownContacts(),
118                                                   kde_tp_filter_contacts_by_publication_status));
119 
120         qCDebug(KTP_KDED_MODULE) << "Watcher is on";
121     } else {
122         qCDebug(KTP_KDED_MODULE) << "Watcher still off, state is" << state << "contactManager is" << contactManager.isNull();
123     }
124 }
125 
onAccountsPresenceStatusFiltered()126 void ContactRequestHandler::onAccountsPresenceStatusFiltered()
127 {
128     qCDebug(KTP_KDED_MODULE) << "Watcher is here";
129     QFutureWatcher< Tp::ContactPtr > *watcher = dynamic_cast< QFutureWatcher< Tp::ContactPtr > * >(sender());
130     qCDebug(KTP_KDED_MODULE) << "Watcher is casted";
131     Tp::Contacts contacts = watcher->future().results().toSet();
132     qCDebug(KTP_KDED_MODULE) << "Watcher is used";
133     if (!contacts.isEmpty()) {
134         onPresencePublicationRequested(contacts);
135     }
136     watcher->deleteLater();
137 }
138 
onPresencePublicationRequested(const Tp::Contacts & contacts)139 void ContactRequestHandler::onPresencePublicationRequested(const Tp::Contacts &contacts)
140 {
141     qCDebug(KTP_KDED_MODULE) << "New contact requested";
142 
143     Q_FOREACH (const Tp::ContactPtr &contact, contacts) {
144         Tp::ContactManagerPtr manager = contact->manager();
145 
146         if (contact->subscriptionState() == Tp::Contact::PresenceStateYes) {
147             Tp::PendingOperation *op = manager->authorizePresencePublication(QList< Tp::ContactPtr >() << contact);
148             op->setProperty("__contact", QVariant::fromValue(contact));
149 
150             connect(op, SIGNAL(finished(Tp::PendingOperation*)),
151                     this, SLOT(onFinalizeSubscriptionFinished(Tp::PendingOperation*)));
152         } else {
153             // Handle multiaccount requests properly
154             if (m_pendingContacts.contains(contact->id())) {
155                 // It's likely we have a simultaneous request
156                 bool newReq = true;
157                 QHash<QString, Tp::ContactPtr>::const_iterator i = m_pendingContacts.constFind(contact->id());
158                 while (i != m_pendingContacts.constEnd() && i.key() == contact->id()) {
159                     if (i.value().data() == contact.data()) {
160                         newReq = false;
161                         break;
162                     }
163                     ++i;
164                 }
165 
166                 if (newReq) {
167                     // Insert multi
168                     m_pendingContacts.insertMulti(contact->id(), contact);
169                 }
170             } else {
171                 // Simple insertion
172                 m_pendingContacts.insert(contact->id(), contact);
173             }
174 
175             connect(contact.data(), SIGNAL(invalidated()), this, SLOT(onContactInvalidated()));
176 
177             updateMenus();
178 
179             if (!m_notifierItem.isNull()) {
180                 m_notifierItem.data()->showMessage(i18n("New contact request"),    //krazy:exclude=qmethods
181                                                    i18n("The contact %1 wants to be able to chat with you.",
182                                                         contact->id()),
183                                                    QLatin1String("list-add-user"));
184             }
185         }
186     }
187 }
188 
onFinalizeSubscriptionFinished(Tp::PendingOperation * op)189 void ContactRequestHandler::onFinalizeSubscriptionFinished(Tp::PendingOperation *op)
190 {
191     Tp::ContactPtr contact = op->property("__contact").value< Tp::ContactPtr >();
192 
193     Q_ASSERT(!contact.isNull());
194 
195     if (op->isError()) {
196         // ARGH
197         if (!m_notifierItem.isNull()) {
198             m_notifierItem.data()->showMessage(i18n("Error adding contact"),
199                                                i18n("%1 has been added successfully to your contact list, "
200                                                     "but might be unable to see when you are online. Error details: %2",
201                                                     contact->alias(), KTp::ErrorDictionary::displayVerboseErrorMessage(op->errorName())),
202                                                QLatin1String("dialog-error"));
203         }
204     } else {
205         // Update the menu
206         m_pendingContacts.remove(contact->id());
207         updateMenus();
208     }
209 }
210 
onContactInvalidated()211 void ContactRequestHandler::onContactInvalidated()
212 {
213     Tp::ContactPtr contact = Tp::ContactPtr(qobject_cast<Tp::Contact*>(sender()));
214 
215     m_pendingContacts.remove(contact->id());
216     updateMenus();
217 }
218 
onNotifierActivated(bool active,const QPoint & pos)219 void ContactRequestHandler::onNotifierActivated(bool active, const QPoint &pos)
220 {
221     if (active) {
222         if (m_notifierItem) {
223             m_notifierItem.data()->contextMenu()->popup(pos);
224         }
225     }
226 }
227 
onContactRequestApproved()228 void ContactRequestHandler::onContactRequestApproved()
229 {
230     QString contactId = qobject_cast<QAction*>(sender())->data().toString();
231 
232     // Disable the action in the meanwhile
233     m_menuItems.value(contactId)->setEnabled(false);
234 
235     if (!contactId.isEmpty()) {
236         QList<Tp::PendingOperation*> operations;
237         QHash<QString, Tp::ContactPtr>::const_iterator i = m_pendingContacts.constFind(contactId);
238         while (i != m_pendingContacts.constEnd() && i.key() == contactId) {
239             if (!i.value()->manager().isNull()) {
240                 Tp::PendingOperation *op = i.value()->manager()->authorizePresencePublication(QList< Tp::ContactPtr >() << i.value());
241                 op->setProperty("__contact", QVariant::fromValue(i.value()));
242                 operations.append(op);
243             }
244             ++i;
245         }
246 
247         // Take the first value, if any
248         if (!operations.isEmpty()) {
249             Tp::ContactPtr contact = m_pendingContacts.find(contactId).value();
250 
251             Tp::PendingComposite *op = new Tp::PendingComposite(operations, true, contact);
252             op->setProperty("__contact", QVariant::fromValue(contact));
253 
254             connect(op, SIGNAL(finished(Tp::PendingOperation*)),
255                     this, SLOT(onAuthorizePresencePublicationFinished(Tp::PendingOperation*)));
256         }
257     }
258 
259 }
260 
onShowContactDetails()261 void ContactRequestHandler::onShowContactDetails()
262 {
263     QString contactId = qobject_cast<QAction*>(sender())->data().toString();
264 
265     if (!contactId.isEmpty()) {
266         const Tp::ContactPtr contact = m_pendingContacts.find(contactId).value();
267         const Tp::ContactManagerPtr manager = contact->manager();
268         Q_FOREACH (const Tp::AccountPtr &account, KTp::accountManager()->allAccounts()) {
269             if (account->connection() == manager->connection()) {
270                 KTp::ContactInfoDialog *dialog = new KTp::ContactInfoDialog(account, contact);
271                 connect(dialog, SIGNAL(closeClicked()), dialog, SLOT(deleteLater()));
272                 dialog->show();
273                 break;
274             }
275         }
276     }
277 }
278 
onAuthorizePresencePublicationFinished(Tp::PendingOperation * op)279 void ContactRequestHandler::onAuthorizePresencePublicationFinished(Tp::PendingOperation *op)
280 {
281     Tp::ContactPtr contact = op->property("__contact").value< Tp::ContactPtr >();
282 
283     if (op->isError()) {
284         if (!m_notifierItem.isNull()) {
285             m_notifierItem.data()->showMessage(i18n("Error granting contact authorization"),
286                                                i18n("There was an error while accepting the request: %1",
287                                                     KTp::ErrorDictionary::displayVerboseErrorMessage(op->errorName())),
288                                                QLatin1String("dialog-error"));
289         }
290 
291         // Re-enable the action
292         m_menuItems.value(contact->id())->setEnabled(true);
293     } else {
294         // op succeeded
295         if (!m_notifierItem.isNull()) {
296             m_notifierItem.data()->showMessage(i18n("Contact request accepted"),
297                                                i18n("%1 will now be able to see when you are online",
298                                                     contact->alias()), QLatin1String("dialog-ok-apply"));
299         }
300 
301         // If needed, reiterate the request on the other end
302         if (contact->manager()->canRequestPresenceSubscription() &&
303                 contact->subscriptionState() == Tp::Contact::PresenceStateNo) {
304 
305             Tp::PendingOperation *op = contact->manager()->requestPresenceSubscription(QList< Tp::ContactPtr >() << contact);
306             op->setProperty("__contact", QVariant::fromValue(contact));
307 
308             connect(op,
309                     SIGNAL(finished(Tp::PendingOperation*)),
310                     this, SLOT(onFinalizeSubscriptionFinished(Tp::PendingOperation*)));
311         } else {
312             // Update the menu
313             m_pendingContacts.remove(contact->id());
314             updateMenus();
315         }
316     }
317 }
318 
onContactRequestDenied()319 void ContactRequestHandler::onContactRequestDenied()
320 {
321     QString contactId = qobject_cast<QAction*>(sender())->data().toString();
322 
323     // Disable the action in the meanwhile
324     m_menuItems.value(contactId)->setEnabled(false);
325 
326     if (!contactId.isEmpty()) {
327         QList<Tp::PendingOperation*> operations;
328         QHash<QString, Tp::ContactPtr>::const_iterator i = m_pendingContacts.constFind(contactId);
329         while (i != m_pendingContacts.constEnd() && i.key() == contactId) {
330             if (!i.value()->manager().isNull()) {
331                 //don't publish our presence to that user
332                 Tp::PendingOperation *op = i.value()->manager()->removePresencePublication(QList< Tp::ContactPtr >() << i.value());
333                 op->setProperty("__contact", QVariant::fromValue(i.value()));
334                 operations.append(op);
335 
336                 //and block that contact
337                 if (i.value()->manager()->canBlockContacts()) {
338                     Tp::PendingOperation *blockOp = i.value()->manager()->blockContacts(QList<Tp::ContactPtr>() << i.value());
339                     operations.append(blockOp);
340                 }
341             }
342             ++i;
343         }
344 
345         // Wait until all operations complete
346         if (!operations.isEmpty()) {
347             Tp::ContactPtr contact = m_pendingContacts.find(contactId).value();
348 
349             Tp::PendingComposite *op = new Tp::PendingComposite(operations, true, contact);
350             op->setProperty("__contact", QVariant::fromValue(contact));
351 
352             connect(op, SIGNAL(finished(Tp::PendingOperation*)),
353                     this, SLOT(onRemovePresencePublicationFinished(Tp::PendingOperation*)));
354         }
355     }
356 }
357 
onRemovePresencePublicationFinished(Tp::PendingOperation * op)358 void ContactRequestHandler::onRemovePresencePublicationFinished(Tp::PendingOperation *op)
359 {
360     Tp::ContactPtr contact = op->property("__contact").value< Tp::ContactPtr >();
361 
362     if (op->isError()) {
363         // ARGH
364         m_notifierItem.data()->showMessage(i18n("Error denying contact request"),
365                                            i18n("There was an error while denying the request: %1",
366                                                 KTp::ErrorDictionary::displayVerboseErrorMessage(op->errorName())),
367                                            QLatin1String("dialog-error"));
368 
369         // Re-enable the action
370         m_menuItems.value(contact->id())->setEnabled(true);
371     } else {
372         // Yeah
373         if (!m_notifierItem.isNull()) {
374             m_notifierItem.data()->showMessage(i18n("Contact request denied"),
375                                                i18n("%1 will not be able to see when you are online",
376                                                     contact->alias()), QLatin1String("dialog-information"));
377         }
378         // Update the menu
379         m_pendingContacts.remove(contact->id());
380         updateMenus();
381     }
382 }
383 
updateMenus()384 void ContactRequestHandler::updateMenus()
385 {
386     if (m_notifierItem.isNull()) {
387         m_notifierItem = new KStatusNotifierItem(QLatin1String("telepathy_kde_contact_requests"), this);
388         m_notifierItem.data()->setCategory(KStatusNotifierItem::Communications);
389         m_notifierItem.data()->setIconByName(QLatin1String("user-identity"));
390         m_notifierItem.data()->setAttentionIconByName(QLatin1String("list-add-user"));
391         m_notifierItem.data()->setStandardActionsEnabled(false);
392         m_notifierItem.data()->setTitle(i18nc("Menu title", "Pending contact requests"));
393         m_notifierItem.data()->setStatus(KStatusNotifierItem::Active);
394 
395         QMenu *notifierMenu = new QMenu(0);
396         notifierMenu->setTitle(i18nc("Context menu title", "Received contact requests"));
397 
398         connect(m_notifierItem.data(), SIGNAL(activateRequested(bool,QPoint)), SLOT(onNotifierActivated(bool,QPoint)));
399 
400         m_notifierItem.data()->setContextMenu(notifierMenu);
401     }
402 
403     qCDebug(KTP_KDED_MODULE) << m_pendingContacts.keys();
404 
405     //add members in pending contacts not in the menu to the menu.
406     QHash<QString, Tp::ContactPtr>::const_iterator i;
407     for (i = m_pendingContacts.constBegin(); i != m_pendingContacts.constEnd(); ++i) {
408         if (m_menuItems.contains(i.key())) {
409             // Skip
410             continue;
411         }
412 
413         qCDebug(KTP_KDED_MODULE);
414         Tp::ContactPtr contact = i.value();
415 
416         QMenu *contactMenu = new QMenu(m_notifierItem.data()->contextMenu());
417         contactMenu->setTitle(i18n("Request from %1", contact->alias()));
418         contactMenu->setObjectName(contact->id());
419 
420         QAction *menuAction;
421 
422         menuAction = new QAction(QIcon(QLatin1String("user-identity")), i18n("Contact Details"), contactMenu);
423         menuAction->setData(i.key());
424         connect(menuAction, SIGNAL(triggered()),
425                 this, SLOT(onShowContactDetails()));
426         contactMenu->addAction(menuAction);
427 
428         if (!contact->publishStateMessage().isEmpty()) {
429             contactMenu->insertSection(menuAction, contact->publishStateMessage());
430         } else {
431             contactMenu->insertSection(menuAction, contact->alias());
432         }
433 
434         contactMenu->addSeparator();
435 
436         menuAction = new QAction(QIcon::fromTheme(QLatin1String("dialog-ok-apply")), i18n("Approve"), contactMenu);
437         menuAction->setData(i.key());
438         connect(menuAction, SIGNAL(triggered()),
439                 this, SLOT(onContactRequestApproved()));
440         contactMenu->addAction(menuAction);
441 
442         menuAction = new QAction(QIcon::fromTheme(QLatin1String("dialog-close")), i18n("Deny"), contactMenu);
443         menuAction->setData(i.key());
444         connect(menuAction, SIGNAL(triggered()),
445                 this, SLOT(onContactRequestDenied()));
446         contactMenu->addAction(menuAction);
447 
448         m_notifierItem.data()->contextMenu()->addMenu(contactMenu);
449         m_menuItems.insert(i.key(), contactMenu);
450     }
451 
452     //remove items that are still in the menu, but not in pending contacts
453     QHash<QString, QMenu*>::iterator j = m_menuItems.begin();
454     while (j != m_menuItems.end()) {
455         if (m_pendingContacts.contains(j.key())) {
456             // Skip
457             ++j;
458             continue;
459         }
460 
461         // Remove
462         m_notifierItem.data()->contextMenu()->removeAction(j.value()->menuAction());
463         j = m_menuItems.erase(j);
464     }
465 
466     if (m_menuItems.size() > 0) {
467         //if menu still contains items, update the tooltip to have the correct number
468         m_notifierItem.data()->setToolTip(QLatin1String("list-add-user"),
469                                           i18np("You have 1 contact wanting to chat with you",
470                                                 "You have %1 contacts wanting to chat with you",
471                                                 m_menuItems.size()),
472                                           QString());
473     } else {
474         //if empty delete the status notifier item
475         m_notifierItem.data()->deleteLater();
476     }
477 
478 }
479 
480 #include "contact-request-handler.moc"
481