Removing nco:Contact updateModified when presence changes. WIP: ut_trackersink
[qtcontacts-tracker:jensg-contactsd.git] / plugins / telepathy / trackersink.cpp
1 /***************************************************************************
2 **
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (people-users@projects.maemo.org)
6 **
7 ** This file is part of contactsd.
8 **
9 ** If you have questions regarding the use of this file, please contact
10 ** Nokia at people-users@projects.maemo.org.
11 **
12 ** This library is free software; you can redistribute it and/or
13 ** modify it under the terms of the GNU Lesser General Public
14 ** License version 2.1 as published by the Free Software Foundation
15 ** and appearing in the file LICENSE.LGPL included in the packaging
16 ** of this file.
17 **
18 ****************************************************************************/
19
20 #include "trackersink.h"
21
22 // contactsd
23 #include <contactphotocopy.h>
24 // QtTracker
25 #include <QtTracker/Tracker>
26 #include <QtTracker/QLive>
27 #include <QtTracker/ontologies/nco.h>
28
29
30 typedef QHash<uint, QSharedPointer<TpContact> > ContactMapHash;
31
32 class TrackerSink::Private
33 {
34 public:
35     Private():transaction_(0) {}
36     ~Private() {
37     }
38     ContactMapHash contactMap;
39     RDFTransactionPtr transaction_;
40     QHash<uint, uint> presenceHash; // maps tpCId hash to presence message hash
41     LiveNodes livenode;
42     QString accountToDelete;
43
44 };
45
46 TrackerSink* TrackerSink::instance()
47 {
48     static TrackerSink instance_;
49     return &instance_;
50 }
51
52 TrackerSink::TrackerSink(): d(new Private)
53 {
54 }
55
56 TrackerSink::~TrackerSink()
57 {
58     delete d;
59 }
60
61 // TODO dead code
62 void TrackerSink::getIMContacts(const QString& /* contact_iri */)
63 {
64     RDFSelect select;
65
66     RDFVariable contact = RDFVariable::fromType<nco::PersonContact>();
67     RDFVariable imaddress = contact.property<nco::hasIMAddress>();
68
69     select.addColumn("distinct", imaddress.property<nco::imID>());
70     select.addColumn("contactId", contact.property<nco::contactLocalUID> ());
71
72     d->livenode = ::tracker()->modelQuery(select);
73
74     connect(d->livenode.model(), SIGNAL(modelUpdated()), this, SLOT(onModelUpdate()));
75
76 }
77
78 // TODO this is dead code - needed in future for merging?
79 void TrackerSink::onModelUpdate()
80 {
81     QStringList idList;
82
83     for (int i = 0 ; i < d->livenode->rowCount() ; i ++) {
84         const QString imAddress = d->livenode->index(i, 0).data().toString();
85         const QString imLocalId = d->livenode->index(i, 1).data().toString();
86
87         bool ok;
88         const uint localId = imLocalId.toUInt(&ok, 0);
89
90         ContactMapHash::const_iterator it = d->contactMap.find(localId);
91         if (it == d->contactMap.end()) {
92             qWarning() << "cannot find TpContact for" << imAddress << localId;
93             continue;
94         }
95
96         const QSharedPointer<const TpContact> contact = it.value();
97         qDebug() << Q_FUNC_INFO << imAddress << imLocalId << ":" << contact;
98
99         saveToTracker(imLocalId, contact.data());
100     }
101
102
103     // TODO unclear logic in this part of dead code. Document.
104     if (d->livenode->rowCount() <= 0) {
105         foreach (const QSharedPointer<TpContact>& contact, d->contactMap.values()) {
106             if (contact.isNull()) {
107                 continue;
108             }
109
110             if(!contact->contact()) {
111                 qWarning() << Q_FUNC_INFO << "lost contact?" << contact;
112                 continue;
113             }
114
115             const QString tpCId = contact->accountPath() + "!" + contact->id();
116
117             const QString id(QString::number(qHash(tpCId)));
118
119             saveToTracker(id, contact.data());
120
121         }
122     }
123 }
124
125 void TrackerSink::connectOnSignals(TpContactPtr contact)
126 {
127     connect(contact.data(), SIGNAL(change(uint, TpContact::ChangeType)),
128             this, SLOT(onChange(uint, TpContact::ChangeType)));
129 }
130
131 TpContact* TrackerSink::find(uint id)
132 {
133     if (d->contactMap.keys().contains(id) ) {
134         return d->contactMap[id].data();
135     }
136
137     return 0;
138 }
139
140 void TrackerSink::onChange(uint uniqueId, TpContact::ChangeType type)
141 {
142
143     TpContact* contact = find(uniqueId);
144     if (!contact) {
145         return;
146     }
147
148     switch (type) {
149     case TpContact::AVATAR_TOKEN:
150         onAvatarUpdated(contact->contact()->id(), contact->avatar(), contact->avatarMime());
151         break;
152     case TpContact::SIMPLE_PRESENCE:
153         onSimplePresenceChanged(contact);
154         break;
155     case TpContact::CAPABILITIES:
156         onCapabilities(contact);
157         break;
158     case TpContact::FEATURES:
159         qDebug() << Q_FUNC_INFO;
160         break;
161     }
162 }
163 void TrackerSink::onFeaturesReady(TpContact* tpContact)
164 {
165     foreach (TpContactPtr contact, d->contactMap) {
166         if (contact.data() == tpContact) {
167             sinkToStorage(contact);
168         }
169     }
170 }
171
172 static QUrl buildContactIri(const QString& contactLocalIdString)
173 {
174   return QUrl(QString::fromLatin1("contact:") + contactLocalIdString);
175 }
176
177 static QUrl buildContactIri(unsigned int contactLocalId)
178 {
179   return buildContactIri(QString::number(contactLocalId));
180 }
181
182 void TrackerSink::saveToTracker(const QString& contactLocalId, const TpContact *tpContact)
183 {
184     const QString& imId = tpContact->id();
185     const QString& nick = tpContact->alias();
186     const QUrl& status = toTrackerStatus(tpContact->presenceType());
187     const QString& msg = tpContact->presenceMessage();
188     const QString& accountpath = tpContact->accountPath();
189
190     const RDFVariable contact(buildContactIri(contactLocalId));
191
192     const QString id(QString::number(TpContact::buildUniqueId(accountpath, imId)));
193     const RDFVariable imAddress(TpContact::buildImAddress(accountpath, imId));
194     qDebug() << Q_FUNC_INFO << accountpath;
195     const RDFVariable imAccount(QUrl(QString::fromLatin1("telepathy:") + accountpath));
196     const RDFVariable imInfo(QUrl(TpContact::buildImAddress(accountpath, imId)));
197     const QDateTime datetime = QDateTime::currentDateTime();
198
199     RDFUpdate addressUpdate;
200
201     addressUpdate.addDeletion(imAddress, nco::imNickname::iri());
202     addressUpdate.addDeletion(imAddress, nco::imPresence::iri());
203     addressUpdate.addDeletion(imAddress, nco::imStatusMessage::iri());
204     addressUpdate.addDeletion(contact, nie::contentLastModified::iri());
205     addressUpdate.addDeletion(imInfo, nie::contentLastModified::iri());
206
207     addressUpdate.addInsertion(RDFStatementList() <<
208                                RDFStatement(imAddress, rdf::type::iri(), nco::IMAddress::iri()) <<
209                                RDFStatement(imAddress, nco::imNickname::iri(), LiteralValue(nick)) <<
210                                RDFStatement(imAddress, nco::imStatusMessage::iri(), LiteralValue((msg))) <<
211                                RDFStatement(imAddress, nco::imPresence::iri(), status) <<
212                                RDFStatement(imAddress, nco::imID::iri(), LiteralValue(imId)) );
213
214     addressUpdate.addInsertion(RDFStatementList() <<
215                                RDFStatement(contact, rdf::type::iri(), nco::PersonContact::iri()) <<
216                                RDFStatement(contact, nco::hasIMAddress::iri(), imAddress) <<
217                                RDFStatement(contact, nco::contactLocalUID::iri(), LiteralValue(id)) <<
218                                RDFStatement(contact, nco::contactUID::iri(), LiteralValue(id)) );
219
220     addressUpdate.addInsertion(RDFStatementList() <<
221                                RDFStatement(imAccount, rdf::type::iri(), nco::IMAccount::iri()) <<
222                                RDFStatement(imAccount, nco::hasIMContact::iri(), imAddress));
223
224     addressUpdate.addInsertion(contact, nie::contentLastModified::iri(), RDFVariable(datetime));
225     addressUpdate.addInsertion(RDFStatementList() <<
226             RDFStatement(imInfo, rdf::type::iri(), nie::InformationElement::iri()) <<
227             RDFStatement(imInfo, nie::contentLastModified::iri(), RDFVariable(datetime)));
228
229     addressUpdate.addDeletion(imAddress, nco::imCapability::iri());
230
231
232
233     if (tpContact->supportsMediaCalls() || tpContact->supportsAudioCalls())  {
234         addressUpdate.addInsertion( RDFStatementList() <<
235                                     RDFStatement(imAddress, nco::imCapability::iri(),
236                                                  nco::im_capability_audio_calls::iri()));
237
238     }
239
240     if (tpContact->supportsTextChats()) {
241         addressUpdate.addInsertion( RDFStatementList() <<
242                                     RDFStatement(imAddress, nco::imCapability::iri(),
243                                                  nco::im_capability_text_chat::iri()));
244     }
245
246     service()->executeQuery(addressUpdate);
247
248 }
249
250 void TrackerSink::sinkToStorage(const QSharedPointer<TpContact>& obj)
251 {
252     if (obj->contact() && obj->contact()->isBlocked()) {
253         return;
254     }
255
256     const unsigned int uniqueId = obj->uniqueId();
257     if (!find(uniqueId) ) {
258         connectOnSignals(obj);
259         d->contactMap[uniqueId] = obj;
260     }
261
262
263     if (!obj->isReady()) {
264         connect(obj.data(), SIGNAL(ready(TpContact*)),
265                 this, SLOT(onFeaturesReady(TpContact*)));
266         return;
267     }
268
269
270     qDebug() << Q_FUNC_INFO <<
271         " \n{Contact Id :" << obj->id() <<
272         " }\n{Alias : " <<  obj->alias() <<
273         " }\n{Status : " << obj->presenceType() <<
274         " }\n{Message : " << obj->presenceMessage() <<
275         " }\n{AccountPath : " << obj->accountPath();
276
277     const QString id = QString::number(contactLocalUID(obj.data()));
278
279     saveToTracker(id, obj.data());
280 }
281
282 void TrackerSink::onCapabilities(TpContact* obj)
283 {
284     if (!isValidTpContact(obj)) {
285         qDebug() << Q_FUNC_INFO << "Invalid Telepathy Contact";
286         return;
287     }
288
289     const RDFVariable imAddress(obj->imAddress());
290
291     RDFUpdate addressUpdate;
292
293     addressUpdate.addDeletion(imAddress, nco::imCapability::iri());
294
295     if (obj->supportsAudioCalls() || obj->supportsMediaCalls()) {
296
297         //TODO: Move this to the constructor, so it's only called once?
298         // This liveNode() call actually sets this RDF triple:
299         //   nco::im_capability_audio_calls a rdfs:Resource
300         // though the libqtttracker maintainer agrees that it's bad API.
301         Live<nco::IMCapability> cap =
302             service()->liveNode(nco::im_capability_audio_calls::iri());
303
304         addressUpdate.addInsertion( RDFStatementList() <<
305                                     RDFStatement(imAddress, nco::imCapability::iri(),
306                                                  nco::im_capability_audio_calls::iri()));
307
308     }
309
310     service()->executeQuery(addressUpdate);
311 }
312
313 // uniqueId - QContactLocalId calculated as TpContact::uniqueId() in TpContact::onSimplePresenceChanged
314 void TrackerSink::onSimplePresenceChanged(TpContact* obj)
315 {
316     qDebug() << Q_FUNC_INFO;
317     if (!isValidTpContact(obj)) {
318         qDebug() << Q_FUNC_INFO << "Invalid Telepathy Contact";
319         return;
320     }
321
322     const RDFVariable imAddress(obj->imAddress());
323     const RDFVariable imInfo(QUrl(TpContact::buildImAddress(obj->accountPath(), obj->contact()->id())));
324     const QDateTime datetime = QDateTime::currentDateTime();
325
326     RDFUpdate addressUpdate;
327
328     addressUpdate.addDeletion(imAddress, nco::imPresence::iri());
329     addressUpdate.addDeletion(imAddress, nco::imStatusMessage::iri());
330     addressUpdate.addDeletion(imInfo, nie::contentLastModified::iri());
331
332     const QSharedPointer<const Tp::Contact> tcontact = obj->contact();
333
334     qDebug() << Q_FUNC_INFO << toTrackerStatus(tcontact->presenceType());
335
336     RDFStatementList insertions;
337     insertions << RDFStatement(imAddress, nco::imStatusMessage::iri(),
338                                LiteralValue(tcontact->presenceMessage()));
339     insertions << RDFStatement(imAddress, nco::imPresence::iri(),
340                                toTrackerStatus(tcontact->presenceType()));
341     addressUpdate.addInsertion(insertions);
342
343     addressUpdate.addInsertion(RDFStatementList() <<
344             RDFStatement(imInfo, rdf::type::iri(), nie::InformationElement::iri()) <<
345             RDFStatement(imInfo, nie::contentLastModified::iri(), RDFVariable(datetime)));
346
347
348     service()->executeQuery(addressUpdate);
349 }
350
351 QList<QSharedPointer<TpContact> > TrackerSink::getFromStorage()
352 {
353     return  d->contactMap.values();
354 }
355
356 bool TrackerSink::contains(const QString& aId)
357 {
358     if (aId.isEmpty()) {
359         return false;
360     }
361
362     RDFVariable rdfContact = RDFVariable::fromType<nco::Contact>();
363     rdfContact.property<nco::hasIMAccount>().property<nco::imID>() = LiteralValue(aId);
364
365     RDFSelect query;
366     query.addColumn(rdfContact);
367     LiveNodes ncoContacts = ::tracker()->modelQuery(query);
368     if (ncoContacts->rowCount() > 0) {
369         return true;
370     }
371
372     return false;
373
374 }
375
376 bool TrackerSink::compareAvatar(const QString& token)
377 {
378     Q_UNUSED(token)
379     return false;
380 }
381
382
383 void TrackerSink::saveAvatarToken(const QString& id, const QString& token, const QString& mime)
384 {
385     const QString avatarPath = ContactPhotoCopy::avatarDir()+"/telepathy_cache"+token+'.'+ mime;
386     qDebug() << Q_FUNC_INFO << token;
387
388     foreach (const QSharedPointer<const TpContact>& c, d->contactMap) {
389         const QSharedPointer<const Tp::Contact> tcontact = c->contact();
390         if (tcontact && id == tcontact->id() ) {
391             using namespace SopranoLive;
392             if (!isValidTpContact(c.data())) {
393                 continue;
394             }
395
396             const QUrl tpUrl = c->imAddress();
397             Live<nco::IMAddress> address = service()->liveNode(tpUrl);
398
399             //TODO: Can this be moved to later, where it is first used?
400             // It has not been moved yet, because some
401             // of these liveNode() calls actually set RDF triples,
402             // though the libqtttracker maintainer agrees that it's bad API.
403             Live<nie::InformationElement> info = service()->liveNode(tpUrl);
404
405             const unsigned int contactLocalId = contactLocalUID(c.data());
406             const QString contactLocalIdString = QString::number(contactLocalId);
407             Live<nco::PersonContact> photoAccount = service()->liveNode(buildContactIri(contactLocalId));
408
409             // set both properties for a transition period
410             // TODO: Set just one when it has been decided:
411             photoAccount->setContactLocalUID(contactLocalIdString);
412             photoAccount->setContactUID(contactLocalIdString);
413
414             const QDateTime datetime = QDateTime::currentDateTime();
415             photoAccount->setContentCreated(datetime);
416             info->setContentLastModified(datetime);
417
418             //FIXME:
419             //To be removed once tracker plugin reads imaddress photo url's
420             Live<nie::DataObject> fileurl = ::tracker()->liveNode(QUrl(avatarPath));
421             photoAccount->setPhoto(fileurl);
422             address->addImAvatar(fileurl);
423
424             break;
425         }
426     }
427 }
428
429 void TrackerSink::onAvatarUpdated(const QString& id, const QString& token, const QString& mime)
430 {
431     if (!compareAvatar(token))
432         saveAvatarToken(id, token, mime);
433 }
434
435 //TODO: This seems to be useless:
436 bool TrackerSink::isValidTpContact(const TpContact *tpContact) {
437     if (tpContact != 0) {
438         return true;
439     }
440
441     return false;
442 }
443
444 RDFServicePtr TrackerSink::service()
445 {
446     if (d->transaction_)
447     {
448         // if transaction was obtained, grab the service from inside it and use it
449         return d->transaction_->service();
450     }
451     else
452     {
453         // otherwise, use tracker directly, with no transactions.
454         return ::tracker();
455     }
456 }
457
458 void TrackerSink::commitTrackerTransaction() {
459     if ( d->transaction_)
460     {
461         d->transaction_->commit();
462         d->transaction_ = RDFTransactionPtr(0); // that's it, transaction lives until commit
463     }
464 }
465
466 void TrackerSink::initiateTrackerTransaction()
467 {
468     if ( !d->transaction_ )
469         d->transaction_ = ::tracker()->createTransaction();
470 }
471
472
473 void TrackerSink::deleteContacts(const QString& path)
474 {
475     //TODO: 
476     //Fix the query when doing contact merging
477
478     d->accountToDelete = path;
479     RDFSelect select;
480
481     RDFVariable contact = RDFVariable::fromType<nco::PersonContact>();
482     RDFVariable imaddress = contact.optional().property<nco::hasIMAddress>();
483     RDFVariable imaccount = RDFVariable::fromType<nco::IMAccount>();
484
485     imaccount.property<nco::hasIMContact>() = imaddress;
486
487     select.addColumn("contact", contact);
488     select.addColumn("distinct", imaddress.property<nco::imID>());
489     select.addColumn("contactId", contact.property<nco::contactLocalUID> ());
490     select.addColumn("accountPath", imaccount);
491     select.addColumn("address", imaddress);
492
493     d->livenode = ::tracker()->modelQuery(select);
494
495     connect(d->livenode.model(), SIGNAL(modelUpdated()), this, SLOT(onDeleteModelReady()));
496
497 }
498
499 void TrackerSink::onDeleteModelReady()
500 {
501      for (int i = 0 ; i < d->livenode->rowCount() ; i ++) {
502         const QString imAddress = d->livenode->index(i, 1).data().toString();
503         const QString imLocalId = d->livenode->index(i, 2).data().toString();
504         const QString imAccountPath = d->livenode->index(i, 3).data().toString();
505         const QString path(imAccountPath.split(":").value(1));
506
507         if ( path == d->accountToDelete) {
508             Live<nco::PersonContact> contact = d->livenode->liveResource<nco::PersonContact>(i, 0);
509             contact->remove();
510             Live<nco::IMAddress> address = d->livenode->liveResource<nco::PersonContact>(i,4);
511             address->remove();
512         }
513      }
514
515 }
516
517 void TrackerSink::clearContacts(const QString& path)
518 {
519     const QList<uint> list (d->contactMap.keys());
520     
521     foreach (uint index, list) {
522         QSharedPointer<TpContact> contact = d->contactMap[index];
523         
524         if (!contact) {
525             continue;
526         }
527         
528         if (contact->accountPath() != path) {
529             continue;
530         }
531         
532         qDebug() << Q_FUNC_INFO << "Removing Contact";
533
534         d->contactMap.remove(index);
535     }
536 }
537
538 /* When account goes offline make all contacts as Unknown
539    this is a specification requirement
540    */
541
542 void TrackerSink::takeAllOffline(const QString& path)
543 {
544     RDFUpdate addressUpdate;
545     foreach (TpContactPtr obj, getFromStorage()) {
546         if (obj->accountPath() != path) {
547             continue;
548         }
549
550         const RDFVariable imAddress(obj->imAddress());
551         const RDFVariable imInfo(QUrl(TpContact::buildImAddress(obj->accountPath(), obj->contact()->id())));
552
553         addressUpdate.addDeletion(imAddress, nco::imPresence::iri());
554         addressUpdate.addDeletion(imAddress, nco::imStatusMessage::iri());
555         addressUpdate.addDeletion(imInfo, nie::contentLastModified::iri());
556
557         const QLatin1String status("unknown");
558         addressUpdate.addInsertion(RDFStatementList() <<
559                                    RDFStatement(imAddress, nco::imStatusMessage::iri(),
560                                                 LiteralValue("")) <<
561                                    RDFStatement(imAddress, nco::imPresence::iri(),
562                                                 toTrackerStatus(status)));
563
564         addressUpdate.addInsertion(imInfo, nie::contentLastModified::iri(),
565                                    RDFVariable(QDateTime::currentDateTime()));
566     }
567
568     service()->executeQuery(addressUpdate);
569 }
570
571 const QUrl & TrackerSink::toTrackerStatus(const uint stat)
572 {
573    switch (stat) {
574        case Tp::ConnectionPresenceTypeUnset: return nco::presence_status_unknown::iri();
575        case Tp::ConnectionPresenceTypeOffline: return nco::presence_status_offline::iri();
576        case Tp::ConnectionPresenceTypeAvailable: return nco::presence_status_available::iri();
577        case Tp::ConnectionPresenceTypeAway: return nco::presence_status_away::iri();
578        case Tp::ConnectionPresenceTypeExtendedAway: return
579                                                     nco::presence_status_extended_away::iri();
580        case Tp::ConnectionPresenceTypeHidden: return nco::presence_status_hidden::iri();
581        case Tp::ConnectionPresenceTypeBusy: return nco::presence_status_busy::iri();
582        case Tp::ConnectionPresenceTypeUnknown: return nco::presence_status_unknown::iri();
583        case Tp::ConnectionPresenceTypeError: return nco::presence_status_error::iri();
584        default: qWarning() << Q_FUNC_INFO << "Presence Status Unknown";
585    }
586
587    return nco::presence_status_error::iri();
588 }
589
590 const QUrl & TrackerSink::toTrackerStatus(const QString& status)
591 {
592     static QHash<QString, QUrl> mapping;
593
594     if (mapping.isEmpty()) {
595         mapping.insert("offline", nco::presence_status_offline::iri());
596         mapping.insert("available", nco::presence_status_available::iri());
597         mapping.insert("away", nco::presence_status_away::iri());
598         mapping.insert("xa", nco::presence_status_extended_away::iri());
599         mapping.insert("dnd", nco::presence_status_busy::iri());
600         mapping.insert("unknown", nco::presence_status_unknown::iri());
601         mapping.insert("hidden", nco::presence_status_hidden::iri());
602         mapping.insert("busy", nco::presence_status_busy::iri());
603     }
604
605     QHash<QString, QUrl>::const_iterator i(mapping.find(status));
606
607     if (i != mapping.end()) {
608         return *i;
609     }
610
611     return nco::presence_status_error::iri();
612 }
613
614 unsigned int TrackerSink::contactLocalUID(const TpContact* const tpContact, bool *existing) const
615 {
616     Q_UNUSED(existing)
617     return tpContact->uniqueId();
618 }