Added StubBase class and SituareService class test.
[situare] / src / situareservice / situareservice.cpp
1 /*
2    Situare - A location system for Facebook
3    Copyright (C) 2010  Ixonos Plc. Authors:
4
5       Henri Lampela - henri.lampela@ixonos.com
6
7    Situare is free software; you can redistribute it and/or
8    modify it under the terms of the GNU General Public License
9    version 2 as published by the Free Software Foundation.
10
11    Situare is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15
16    You should have received a copy of the GNU General Public License
17    along with Situare; if not, write to the Free Software
18    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
19    USA.
20 */
21
22 #include "parser.h"
23
24 #include <QtAlgorithms>
25 #include <QDebug>
26 #include <QtNetwork/QNetworkReply>
27 #include <QPixmap>
28 #include <QStringList>
29 #include <QtGlobal>
30
31 #include "database.h"
32 #include "error.h"
33 #include "network/networkaccessmanager.h"
34 #include "situarecommon.h"
35 #include "ui/avatarimage.h"
36
37 #include "situareservice.h"
38
39 SituareService::SituareService(NetworkAccessManager *networkManager, ImageFetcher *imageFetcher,
40                                QObject *parent)
41         : QObject(parent),
42         m_user(0)
43 {
44     qDebug() << __PRETTY_FUNCTION__;
45
46     m_networkManager = networkManager;
47     connect(m_networkManager, SIGNAL(finished(QNetworkReply*)),
48             this, SLOT(requestFinished(QNetworkReply*)), Qt::QueuedConnection);
49
50     m_imageFetcher = imageFetcher;
51     connect(this, SIGNAL(fetchImage(QString, QUrl)),
52             m_imageFetcher, SLOT(fetchImage(QString, QUrl)));
53     connect(m_imageFetcher, SIGNAL(imageReceived(QString,QPixmap)),
54             this, SLOT(imageReceived(QString,QPixmap)));
55     connect(m_imageFetcher, SIGNAL(error(int, int)),
56             this, SIGNAL(error(int, int)));
57
58     m_database = new Database(this);
59     m_database->openDatabase();
60     m_database->createNotificationTable();
61     m_database->createUserTable();
62     m_database->createTagTable();
63     m_database->createUserTagTable();
64 }
65
66 SituareService::~SituareService()
67 {
68     qDebug() << __PRETTY_FUNCTION__;
69
70     if(m_user) {
71         delete m_user;
72         m_user = 0;
73     }
74
75     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
76     m_friendsList.clear();
77 }
78
79 void SituareService::fetchNotifications()
80 {
81     qDebug() << __PRETTY_FUNCTION__;
82
83     QByteArray arr = m_database->getNotifications(m_user->userId().toULongLong());
84
85     parseNotificationsData(arr);
86 }
87
88 void SituareService::fetchPeopleWithSimilarInterest(const GeoCoordinate &southWestCoordinates,
89                                                     const GeoCoordinate &northEastCoordinates)
90 {
91     qDebug() << __PRETTY_FUNCTION__;
92
93     QByteArray arr = m_database->getInterestingPeople(m_user->userId().toULongLong(),
94                                                       southWestCoordinates,
95                                                       northEastCoordinates);
96
97     parseInterestingPeopleData(arr);
98 }
99
100 void SituareService::fetchLocations()
101 {
102     qDebug() << __PRETTY_FUNCTION__;
103
104     QString cookie = formCookie(API_KEY, m_credentials.expires(), m_credentials.userID(),
105                                 m_credentials.sessionKey(), m_credentials.sessionSecret(),
106                                 m_credentials.sig(), EN_LOCALE);
107
108     QUrl url = formUrl(SITUARE_URL, GET_LOCATIONS);
109     sendRequest(url, COOKIE, cookie);
110 }
111
112 void SituareService::reverseGeo(const GeoCoordinate &coordinates)
113 {
114     qDebug() << __PRETTY_FUNCTION__;
115
116     QString cookie = formCookie(API_KEY, m_credentials.expires(),m_credentials.userID(),
117                                 m_credentials.sessionKey(), m_credentials.sessionSecret(),
118                                 m_credentials.sig(), EN_LOCALE);
119
120     QString urlParameters = formUrlParameters(coordinates);
121     urlParameters.append(JSON_FORMAT);
122     QUrl url = formUrl(SITUARE_URL, REVERSE_GEO, urlParameters);
123
124     sendRequest(url, COOKIE, cookie);
125 }
126
127 void SituareService::updateLocation(const GeoCoordinate &coordinates, const QString &status,
128                                     const bool &publish)
129 {
130     qDebug() << __PRETTY_FUNCTION__;
131
132     QString urlParameters = formUrlParameters(coordinates, status, publish);
133     QUrl url = formUrl(SITUARE_URL, UPDATE_LOCATION, urlParameters);
134
135     QString cookie = formCookie(API_KEY, m_credentials.expires(), m_credentials.userID(),
136                                 m_credentials.sessionKey(), m_credentials.sessionSecret(),
137                                 m_credentials.sig(), EN_LOCALE);
138
139     sendRequest(url, COOKIE, cookie);
140 }
141
142 QString SituareService::formCookie(const QString &apiKeyValue, QString expiresValue,
143                                    QString userValue, QString sessionKeyValue,
144                                    QString sessionSecretValue, const QString &signatureValue,
145                                    const QString &localeValue)
146 {
147     qDebug() << __PRETTY_FUNCTION__;
148
149     QString cookie;
150     QString apiKey;
151     QString user;
152     QString expires;
153     QString sessionKey;
154     QString sessionSecret;
155     QString locale;
156     QString variable;
157     QString signature = EQUAL_MARK;
158     QStringList variableList;
159
160     signature.append(signatureValue);
161     apiKey.append(apiKeyValue);
162     apiKey.append(UNDERLINE_MARK);
163
164     user.append(USER);
165     user.append(EQUAL_MARK);
166     expires.append(EXPIRES);
167     expires.append(EQUAL_MARK);
168     sessionKey.append(SESSION_KEY);
169     sessionKey.append(EQUAL_MARK);
170     sessionSecret.append(SESSION_SECRET);
171     sessionSecret.append(EQUAL_MARK);
172     locale.append(LOCALE);
173     locale.append(EQUAL_MARK);
174     locale.append(localeValue);
175
176     variableList.append(expires.append(expiresValue.append(BREAK_MARK)));
177     variableList.append(sessionKey.append(sessionKeyValue.append(BREAK_MARK)));
178     variableList.append(user.append(userValue).append(BREAK_MARK));
179     variableList.append(sessionSecret.append(sessionSecretValue.append(BREAK_MARK)));
180
181     cookie.append(BREAK_MARK);
182
183     foreach(variable, variableList) {
184         cookie.append(apiKey);
185         cookie.append(variable);
186     }
187     apiKey.remove(UNDERLINE_MARK);
188     cookie.append(apiKey);
189     cookie.append(signature);
190     cookie.append(BREAK_MARK);
191     cookie.append(locale);
192
193     qDebug() << cookie;
194
195     return cookie;
196 }
197
198 QUrl SituareService::formUrl(const QString &baseUrl, const QString &phpScript,
199                              QString urlParameters)
200 {
201     qDebug() << __PRETTY_FUNCTION__;
202     QString urlString;
203
204     urlString.append(baseUrl);
205     urlString.append(phpScript);
206     if(!urlParameters.isEmpty())
207         urlString.append(urlParameters);
208
209     QUrl url = QUrl(urlString);
210
211     qDebug() << url;
212
213     return url;
214 }
215
216 QString SituareService::formUrlParameters(const GeoCoordinate &coordinates, QString status,
217                                           bool publish)
218 {
219     qDebug() << __PRETTY_FUNCTION__;
220
221     // one scene pixel is about 5.4e-6 degrees, the integer part is max three digits and one
222     // additional digit is added for maximum precision
223     const int COORDINATE_PRECISION = 10;
224
225     QString parameters;
226
227     parameters.append(QUESTION_MARK);
228     parameters.append(LATITUDE);
229     parameters.append(EQUAL_MARK);
230     parameters.append(QString::number(coordinates.latitude(), 'f', COORDINATE_PRECISION));
231     parameters.append(AMBERSAND_MARK);
232     parameters.append(LONGTITUDE);
233     parameters.append(EQUAL_MARK);
234     parameters.append(QString::number(coordinates.longitude(), 'f', COORDINATE_PRECISION));
235
236     parameters.append(AMBERSAND_MARK);
237     parameters.append(PUBLISH);
238     parameters.append(EQUAL_MARK);
239
240     if(publish)
241         parameters.append(PUBLISH_TRUE);
242     else
243         parameters.append(PUBLISH_FALSE);
244
245     if(!status.isEmpty()) {
246         parameters.append(AMBERSAND_MARK);
247         parameters.append(DATA);
248         parameters.append(EQUAL_MARK);
249         parameters.append(status);
250     }
251
252     return parameters;
253 }
254
255 void SituareService::sendRequest(const QUrl &url, const QString &cookieType, const QString &cookie)
256 {
257     qDebug() << __PRETTY_FUNCTION__;
258
259     QNetworkRequest request;
260
261     request.setUrl(url);
262     request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
263     request.setRawHeader(cookieType.toAscii(), cookie.toUtf8());
264
265     QNetworkReply *reply = m_networkManager->get(request, true);
266
267     m_currentRequests.append(reply);
268 }
269
270 void SituareService::requestFinished(QNetworkReply *reply)
271 {
272     qDebug() << __PRETTY_FUNCTION__;
273
274     //Reply from situare
275     if (m_currentRequests.contains(reply)) {
276
277         qDebug() << "BytesAvailable: " << reply->bytesAvailable();
278
279         if (reply->error()) {
280             emit error(ErrorContext::NETWORK, reply->error());
281         } else {
282             QByteArray replyArray = reply->readAll();
283             qDebug() << "Reply from: " << reply->url() << "reply " << replyArray;
284
285             if(replyArray == ERROR_LAT.toAscii()) {
286                 qDebug() << "Error: " << ERROR_LAT;
287                 emit error(ErrorContext::SITUARE, SituareError::UPDATE_FAILED);
288             } else if(replyArray == ERROR_LON.toAscii()) {
289                 qDebug() << "Error: " << ERROR_LON;
290                 emit error(ErrorContext::SITUARE, SituareError::UPDATE_FAILED);
291             } else if(replyArray.contains(ERROR_SESSION.toAscii())) {
292                 qDebug() << "Error: " << ERROR_SESSION;
293                 emit error(ErrorContext::SITUARE, SituareError::SESSION_EXPIRED);
294             } else if(replyArray.startsWith(OPENING_BRACE_MARK.toAscii())) {
295                 qDebug() << "JSON string";
296                 parseUserData(replyArray);
297             } else if(replyArray.isEmpty()) {
298                 if(reply->url().toString().contains(UPDATE_LOCATION.toAscii())) {
299                     emit updateWasSuccessful();
300                 } else {
301                     // session credentials are invalid
302                     emit error(ErrorContext::SITUARE, SituareError::SESSION_EXPIRED);
303                 }
304             } else {
305                 // unknown reply
306                 emit error(ErrorContext::SITUARE, SituareError::ERROR_GENERAL);
307             }
308         }
309         m_currentRequests.removeAll(reply);
310         reply->deleteLater();
311     }
312 }
313
314 void SituareService::credentialsReady(const FacebookCredentials &credentials)
315 {
316     qDebug() << __PRETTY_FUNCTION__;
317
318     m_credentials = credentials;
319 }
320
321 void SituareService::parseInterestingPeopleData(const QByteArray &jsonReply)
322 {
323     qDebug() << __PRETTY_FUNCTION__;
324
325     QJson::Parser parser;
326     bool ok;
327
328     QVariantMap result = parser.parse(jsonReply, &ok).toMap();
329
330     if (!ok) {
331         emit error(ErrorContext::SITUARE, SituareError::INVALID_JSON);
332         return;
333     } else {
334         QList<User> interestingPeople;
335
336         foreach (QVariant personVariant, result["people"].toList()) {
337             User user;
338             QMap<QString, QVariant> person = personVariant.toMap();
339             user.setUserId(person["uid"].toString());
340             user.setName(person["name"].toString());
341             user.setProfileImage(AvatarImage::create(
342                     QPixmap(":/res/images/empty_avatar.png"), AvatarImage::Small));
343             user.setProfileImageUrl(person["image_url"].toUrl());
344
345             interestingPeople.append(user);
346
347             emit fetchImage(user.userId(), user.profileImageUrl());
348         }
349
350         emit interestingPeopleReceived(interestingPeople);
351     }
352 }
353
354 void SituareService::parseNotificationsData(const QByteArray &jsonReply)
355 {
356     QJson::Parser parser;
357     bool ok;
358
359     QVariantMap result = parser.parse(jsonReply, &ok).toMap();
360
361     if (!ok) {
362         emit error(ErrorContext::SITUARE, SituareError::INVALID_JSON);
363         return;
364     } else {
365         QList<Notification> notifications;
366
367         foreach (QVariant notificationVariant, result["notifications"].toList()) {
368             Notification notification;
369             QMap<QString, QVariant> notificationMap = notificationVariant.toMap();
370             notification.setId(notificationMap["id"].toString());
371             notification.setSenderId(notificationMap["sender_id"].toString());
372             notification.setSenderName(notificationMap["sender_name"].toString());
373             uint timestampSeconds = notificationMap["timestamp"].toUInt();
374             notification.setTimestamp(QDateTime::fromTime_t(timestampSeconds));
375             notification.setText(notificationMap["text"].toString());
376             notification.setImage(AvatarImage::create(
377                     QPixmap(":/res/images/empty_avatar.png"), AvatarImage::Small));
378
379             notifications.append(notification);
380
381             emit fetchImage(notification.id(), notificationMap["image_url"].toString());
382         }
383
384         emit notificationsReceived(notifications);
385     }
386 }
387
388 void SituareService::parseUserData(const QByteArray &jsonReply)
389 {
390     qDebug() << __PRETTY_FUNCTION__;
391
392     m_defaultImage = false;
393
394     QJson::Parser parser;
395     bool ok;
396
397     QVariantMap result = parser.parse (jsonReply, &ok).toMap();
398
399     if (!ok) {
400         emit error(ErrorContext::SITUARE, SituareError::INVALID_JSON);
401         return;
402     } else {
403
404         if(result.contains("ErrorCode")) {
405             QVariant errorVariant = result.value("ErrorCode");
406             emit error(ErrorContext::SITUARE, errorVariant.toInt());
407             return;
408         } else if(result.contains("user")) {
409
410             QVariant userVariant = result.value("user");
411             QMap<QString, QVariant> userMap = userVariant.toMap();
412
413             GeoCoordinate coordinates(userMap["latitude"].toReal(), userMap["longitude"].toReal());
414
415             QUrl imageUrl = userMap[NORMAL_SIZE_PROFILE_IMAGE].toUrl();
416
417             QString address = userMap["address"].toString();
418             if(address.isEmpty()) {
419                 QStringList location;
420                 location.append(QString::number(coordinates.latitude()));
421                 location.append(QString::number(coordinates.longitude()));
422                 address = location.join(", ");
423             }
424
425             User user = User(address, coordinates, userMap["name"].toString(),
426                           userMap["note"].toString(), imageUrl, userMap["timestamp"].toString(),
427                           true, userMap["uid"].toString());
428
429             if(imageUrl.isEmpty()) {
430                 // user doesn't have profile image, so we need to get him a silhouette image
431                 m_defaultImage = true;
432                 user.setProfileImage(AvatarImage::create(
433                         QPixmap(":/res/images/empty_avatar_big.png"), AvatarImage::Large));
434             }
435
436             QList<User> tmpFriendsList;
437
438             foreach (QVariant friendsVariant, result["friends"].toList()) {
439               QMap<QString, QVariant> friendMap = friendsVariant.toMap();
440               QVariant distance = friendMap["distance"];
441               QMap<QString, QVariant> distanceMap = distance.toMap();
442
443               GeoCoordinate coordinates(friendMap["latitude"].toReal(),friendMap["longitude"].toReal());
444
445               QUrl imageUrl = friendMap["profile_pic"].toUrl();
446
447               QString address = friendMap["address"].toString();
448               if(address.isEmpty()) {
449                   QStringList location;
450                   location.append(QString::number(coordinates.latitude()));
451                   location.append(QString::number(coordinates.longitude()));
452                   address = location.join(", ");
453               }
454
455               User buddy = User(address, coordinates, friendMap["name"].toString(),
456                                friendMap["note"].toString(), imageUrl,
457                                friendMap["timestamp"].toString(),
458                                false, friendMap["uid"].toString(), distanceMap["units"].toString(),
459                                distanceMap["value"].toDouble());
460
461               if(imageUrl.isEmpty()) {
462                   // friend doesn't have profile image, so we need to get him a silhouette image
463                   m_defaultImage = true;
464                   buddy.setProfileImage(AvatarImage::create(
465                           QPixmap(":/res/images/empty_avatar.png"), AvatarImage::Small));
466               }
467
468               tmpFriendsList.append(buddy);
469             }
470
471             QHash<QString, QUrl> imageUrlList; // url list for images
472
473             // set unchanged profile images or add new images to imageUrlList for downloading
474             if(m_user) {
475                 if(m_user->profileImageUrl() != user.profileImageUrl()) {
476                     if(!user.profileImageUrl().isEmpty())
477                         imageUrlList.insert(user.userId(), user.profileImageUrl());
478                 } else {
479                     user.setProfileImage(m_user->profileImage());
480                 }
481             } else {
482                 if(!user.profileImageUrl().isEmpty())
483                     imageUrlList.insert(user.userId(), user.profileImageUrl());
484             }
485
486             // clear old user object
487             if(m_user) {
488                 delete m_user;
489                 m_user = 0;
490             }
491
492             // create new user object from temporary user object
493             m_user = new User(user);
494
495             // set unchanged profile images or add new images to imageUrlList for downloading
496             if(!m_friendsList.isEmpty()) {
497                 foreach(User tmpBuddy, tmpFriendsList) {
498                     if(!tmpBuddy.profileImageUrl().isEmpty()) {
499                         bool found = false;
500                         foreach(User *buddy, m_friendsList) {
501                             if(tmpBuddy.profileImageUrl() == buddy->profileImageUrl()) {
502                                 tmpBuddy.setProfileImage(buddy->profileImage());
503                                 found = true;
504                                 break;
505                             }
506                         }
507                         if(!found && !tmpBuddy.profileImageUrl().isEmpty())
508                             imageUrlList.insert(tmpBuddy.userId(), tmpBuddy.profileImageUrl());
509                     }
510                 }
511             } else {
512                 foreach(User buddy, tmpFriendsList) {
513                     if(!buddy.profileImageUrl().isEmpty())
514                         imageUrlList.insert(buddy.userId(), buddy.profileImageUrl());
515                 }
516             }
517
518             // clear old friendlist
519             qDeleteAll(m_friendsList.begin(), m_friendsList.end());
520             m_friendsList.clear();
521
522             // populate new friendlist with temporary friendlist's data
523             foreach(User tmpFriendItem, tmpFriendsList) {
524                 User *friendItem = new User(tmpFriendItem);
525                 m_friendsList.append(friendItem);
526             }
527             tmpFriendsList.clear();
528
529             //REMOVE WHEN NOT NEEDED! get user tags and set tags to the user
530             m_user->setTags(getTags(m_user->userId()));
531
532             emit userDataChanged(m_user, m_friendsList);
533
534             // set silhouette image to imageUrlList for downloading
535             if(m_defaultImage)
536                 imageUrlList.insert("", QUrl(SILHOUETTE_URL));
537
538             addProfileImages(imageUrlList);
539             imageUrlList.clear();
540
541         } else {
542             QVariant address = result.value("address");
543             if(!address.toString().isEmpty()) {
544                 emit reverseGeoReady(address.toString());
545             } else {
546                 QStringList coordinates;
547                 coordinates.append(result.value("lat").toString());
548                 coordinates.append(result.value("lon").toString());
549
550                 emit error(ErrorContext::SITUARE, SituareError::ADDRESS_RETRIEVAL_FAILED);
551                 emit reverseGeoReady(coordinates.join(", "));
552             }
553         }
554     }
555 }
556
557 void SituareService::imageReceived(const QString &id, const QPixmap &image)
558 {
559     qDebug() << __PRETTY_FUNCTION__;
560
561     if (m_user->userId() == id)
562         emit imageReady(id, AvatarImage::create(image, AvatarImage::Large));
563     else
564         emit imageReady(id, AvatarImage::create(image, AvatarImage::Small));
565 }
566
567 void SituareService::addProfileImages(const QHash<QString, QUrl> &imageUrlList)
568 {
569     qDebug() << __PRETTY_FUNCTION__;
570
571     QHashIterator<QString, QUrl> imageUrlListIterator(imageUrlList);
572
573     while (imageUrlListIterator.hasNext()) {
574         imageUrlListIterator.next();
575         emit fetchImage(imageUrlListIterator.key(), imageUrlListIterator.value());
576     }
577 }
578
579 void SituareService::clearUserData()
580 {
581     qDebug() << __PRETTY_FUNCTION__;
582
583     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
584     m_friendsList.clear();
585
586     if(m_user) {
587         delete m_user;
588         m_user = 0;
589     }
590     emit userDataChanged(m_user, m_friendsList);
591 }
592
593 QStringList SituareService::getTags(const QString &userId)
594 {
595     qDebug() << __PRETTY_FUNCTION__;
596
597     return m_database->getTags(userId.toInt());
598 }
599
600 void SituareService::updateTags(const QString &userId, const QStringList &tags)
601 {
602     qDebug() << __PRETTY_FUNCTION__;
603
604     foreach (QString tag, tags)
605         m_database->addTag(userId.toInt(), tag);
606 }