538d4324ffdca4509f0b8952a2738fc98f9683c3
[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 <qjson/parser.h>
23
24 #include <QDebug>
25 #include <QNetworkReply>
26 #include <QPixmap>
27 #include <QStringList>
28 #include <QtAlgorithms>
29 #include <QtGlobal>
30
31 #include "error.h"
32 #include "network/networkaccessmanager.h"
33 #include "situarecommon.h"
34 #include "ui/avatarimage.h"
35
36 #include "situareservice.h"
37
38 SituareService::SituareService(QObject *parent)
39         : QObject(parent),
40         m_user(0)
41 {
42     qDebug() << __PRETTY_FUNCTION__;
43
44     m_networkManager = new NetworkAccessManager(this);
45     connect(m_networkManager, SIGNAL(finished(QNetworkReply*)),
46             this, SLOT(requestFinished(QNetworkReply*)), Qt::QueuedConnection);
47
48     m_imageFetcher = new ImageFetcher(new NetworkAccessManager(this), this);
49     connect(this, SIGNAL(fetchImage(QUrl)),
50             m_imageFetcher, SLOT(fetchImage(QUrl)));
51     connect(m_imageFetcher, SIGNAL(imageReceived(QUrl,QPixmap)),
52             this, SLOT(imageReceived(QUrl, QPixmap)));
53     connect(m_imageFetcher, SIGNAL(error(int, int)),
54             this, SIGNAL(error(int, int)));
55 }
56
57 SituareService::~SituareService()
58 {
59     qDebug() << __PRETTY_FUNCTION__;
60
61     if(m_user) {
62         delete m_user;
63         m_user = 0;
64     }
65
66     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
67     m_friendsList.clear();
68 }
69
70 void SituareService::addProfileImages(const QList<QUrl> &imageUrlList)
71 {
72     qDebug() << __PRETTY_FUNCTION__;
73
74     foreach(QUrl url, imageUrlList) {
75         emit fetchImage(url);
76     }
77 }
78
79 void SituareService::appendAccessToken(QString &requestUrl)
80 {
81     qWarning() << __PRETTY_FUNCTION__;
82
83 //    requestUrl.append("access_token=");
84     requestUrl.append(m_session);
85
86 //    qWarning() << __PRETTY_FUNCTION__ << "request url with parameters and access token:" << requestUrl;
87 }
88
89 void SituareService::buildRequest(const QString &script, const QHash<QString, QString> &parameters)
90 {
91     qWarning() << __PRETTY_FUNCTION__;
92
93     const QString PARAMETER_KEY_API = "api";
94     const QString PARAMETER_VALUE_API = "2.0";
95
96     QString url = SITUARE_URL;
97     url.append(script);
98     url.append("?");
99
100     // append default api version parameter if not yet specified
101 //    if (!parameters.contains(PARAMETER_KEY_API))
102 //        url.append(PARAMETER_KEY_API + "=" + PARAMETER_VALUE_API + "&");
103
104     // append parameters
105     if (!parameters.isEmpty()) {
106         QHash<QString, QString>::const_iterator i = parameters.constBegin();
107         while (i != parameters.constEnd()) {
108             url.append(i.key());
109             url.append("=");
110             url.append(i.value());
111             url.append("&");
112             i++;
113         }
114     }
115
116 //    qWarning() << __PRETTY_FUNCTION__ << "request url with parameters:" << url;
117
118     if (!m_session.isEmpty()) {
119         appendAccessToken(url);
120         sendRequest(url);
121     } else {
122         m_requestsWaitingAccessToken.append(url);
123         ///< @todo emit login request
124     }
125 }
126
127 void SituareService::clearUserData()
128 {
129     qDebug() << __PRETTY_FUNCTION__;
130
131     qDeleteAll(m_friendsList.begin(), m_friendsList.end());
132     m_friendsList.clear();
133
134     if(m_user) {
135         delete m_user;
136         m_user = 0;
137     }
138     emit userDataChanged(m_user, m_friendsList);
139 }
140
141 QString SituareService::degreesToString(double degrees)
142 {
143     qDebug() << __PRETTY_FUNCTION__;
144
145     // one scene pixel is about 5.4e-6 degrees, the integer part is max three digits and one
146     // additional digit is added for maximum precision
147     const int PRECISION = 10;
148
149     return QString::number(degrees, 'f', PRECISION);
150 }
151
152 void SituareService::fetchLocations()
153 {
154     qDebug() << __PRETTY_FUNCTION__;
155
156     QHash<QString, QString> parameters;
157     parameters.insert("extra_user_data", NORMAL_SIZE_PROFILE_IMAGE);
158
159     buildRequest(GET_LOCATIONS, parameters);
160 }
161
162 QString SituareService::formCookie(const QString &apiKeyValue, QString expiresValue,
163                                    QString userValue, QString sessionKeyValue,
164                                    QString sessionSecretValue, const QString &signatureValue,
165                                    const QString &localeValue)
166 {
167     qDebug() << __PRETTY_FUNCTION__;
168
169     QString cookie;
170     QString apiKey;
171     QString user;
172     QString expires;
173     QString sessionKey;
174     QString sessionSecret;
175     QString locale;
176     QString variable;
177     QString signature = EQUAL_MARK;
178     QStringList variableList;
179
180     signature.append(signatureValue);
181     apiKey.append(apiKeyValue);
182     apiKey.append(UNDERLINE_MARK);
183
184     user.append(USER);
185     user.append(EQUAL_MARK);
186     expires.append(EXPIRES);
187     expires.append(EQUAL_MARK);
188     sessionKey.append(SESSION_KEY);
189     sessionKey.append(EQUAL_MARK);
190     sessionSecret.append(SESSION_SECRET);
191     sessionSecret.append(EQUAL_MARK);
192     locale.append(LOCALE);
193     locale.append(EQUAL_MARK);
194     locale.append(localeValue);
195
196     variableList.append(expires.append(expiresValue.append(BREAK_MARK)));
197     variableList.append(sessionKey.append(sessionKeyValue.append(BREAK_MARK)));
198     variableList.append(user.append(userValue).append(BREAK_MARK));
199     variableList.append(sessionSecret.append(sessionSecretValue.append(BREAK_MARK)));
200
201     cookie.append(BREAK_MARK);
202
203     foreach(variable, variableList) {
204         cookie.append(apiKey);
205         cookie.append(variable);
206     }
207     apiKey.remove(UNDERLINE_MARK);
208     cookie.append(apiKey);
209     cookie.append(signature);
210     cookie.append(BREAK_MARK);
211     cookie.append(locale);
212
213     qDebug() << cookie;
214
215     return cookie;
216 }
217
218 QUrl SituareService::formUrl(const QString &baseUrl, const QString &phpScript,
219                              QString urlParameters)
220 {
221     qDebug() << __PRETTY_FUNCTION__;
222     QString urlString;
223
224     urlString.append(baseUrl);
225     urlString.append(phpScript);
226     if(!urlParameters.isEmpty())
227         urlString.append(urlParameters);
228
229     QUrl url = QUrl(urlString);
230
231     qDebug() << url;
232
233     return url;
234 }
235
236 QString SituareService::formUrlParameters(const GeoCoordinate &coordinates, QString status,
237                                           bool publish)
238 {
239     qDebug() << __PRETTY_FUNCTION__;
240
241     // one scene pixel is about 5.4e-6 degrees, the integer part is max three digits and one
242     // additional digit is added for maximum precision
243     const int COORDINATE_PRECISION = 10;
244
245     QString parameters;
246
247     parameters.append(QUESTION_MARK);
248     parameters.append(LATITUDE);
249     parameters.append(EQUAL_MARK);
250     parameters.append(QString::number(coordinates.latitude(), 'f', COORDINATE_PRECISION));
251     parameters.append(AMBERSAND_MARK);
252     parameters.append(LONGTITUDE);
253     parameters.append(EQUAL_MARK);
254     parameters.append(QString::number(coordinates.longitude(), 'f', COORDINATE_PRECISION));
255
256     parameters.append(AMBERSAND_MARK);
257     parameters.append(PUBLISH);
258     parameters.append(EQUAL_MARK);
259
260     if(publish)
261         parameters.append(PUBLISH_TRUE);
262     else
263         parameters.append(PUBLISH_FALSE);
264
265     if(!status.isEmpty()) {
266         parameters.append(AMBERSAND_MARK);
267         parameters.append(DATA);
268         parameters.append(EQUAL_MARK);
269         parameters.append(status);
270     }
271
272     return parameters;
273 }
274
275 void SituareService::imageReceived(const QUrl &url, const QPixmap &image)
276 {
277     qDebug() << __PRETTY_FUNCTION__;
278     qDebug() << "Image URL: " << url << " size :" << image.size();
279
280     // assign facebook silhouette image to all who doesn't have a profile image
281     if(url == QUrl(SILHOUETTE_URL)) {
282         if(m_user->profileImageUrl().isEmpty()) {
283             m_user->setProfileImage(AvatarImage::create(image, AvatarImage::Large));
284             emit imageReady(m_user);
285         }
286         foreach(User *friendItem, m_friendsList) {
287             if(friendItem->profileImageUrl().isEmpty()) {
288                 friendItem->setProfileImage(AvatarImage::create(image, AvatarImage::Small));
289                 emit imageReady(friendItem);
290             }
291         }
292     }
293
294     if (m_user->profileImageUrl() == url) {
295         m_user->setProfileImage(AvatarImage::create(image, AvatarImage::Large));
296         emit imageReady(m_user);
297     }
298
299     foreach(User *friendItem, m_friendsList) {
300         if(friendItem->profileImageUrl() == url) {
301             friendItem->setProfileImage(AvatarImage::create(image, AvatarImage::Small));
302             emit imageReady(friendItem);
303         }
304     }
305 }
306
307 void SituareService::parseUserData(const QByteArray &jsonReply)
308 {
309     qDebug() << __PRETTY_FUNCTION__;
310
311     m_defaultImage = false;
312
313     QJson::Parser parser;
314     bool ok;
315
316     QVariantMap result = parser.parse (jsonReply, &ok).toMap();
317     if (!ok) {
318         emit error(ErrorContext::SITUARE, SituareError::INVALID_JSON);
319         return;
320     } else {
321
322         if(result.contains("ErrorCode")) {
323             QVariant errorVariant = result.value("ErrorCode");
324             emit error(ErrorContext::SITUARE, errorVariant.toInt());
325             return;
326         } else if(result.contains("user")) {
327
328             QVariant userVariant = result.value("user");
329             QMap<QString, QVariant> userMap = userVariant.toMap();
330
331             GeoCoordinate coordinates(userMap["latitude"].toReal(), userMap["longitude"].toReal());
332
333             QUrl imageUrl = userMap[NORMAL_SIZE_PROFILE_IMAGE].toUrl();
334
335             if(imageUrl.isEmpty()) {
336                 // user doesn't have profile image, so we need to get him a silhouette image
337                 m_defaultImage = true;
338             }
339
340             QString address = userMap["address"].toString();
341             if(address.isEmpty()) {
342                 QStringList location;
343                 location.append(QString::number(coordinates.latitude()));
344                 location.append(QString::number(coordinates.longitude()));
345                 address = location.join(", ");
346             }
347
348             User user = User(address, coordinates, userMap["name"].toString(),
349                           userMap["note"].toString(), imageUrl, userMap["timestamp"].toString(),
350                           true, userMap["uid"].toString());
351
352             QList<User> tmpFriendsList;
353
354             foreach (QVariant friendsVariant, result["friends"].toList()) {
355               QMap<QString, QVariant> friendMap = friendsVariant.toMap();
356               QVariant distance = friendMap["distance"];
357               QMap<QString, QVariant> distanceMap = distance.toMap();
358
359               GeoCoordinate coordinates(friendMap["latitude"].toReal(),friendMap["longitude"].toReal());
360
361               QUrl imageUrl = friendMap["profile_pic"].toUrl();
362
363               if(imageUrl.isEmpty()) {
364                   // friend doesn't have profile image, so we need to get him a silhouette image
365                   m_defaultImage = true;
366               }
367
368               QString address = friendMap["address"].toString();
369               if(address.isEmpty()) {
370                   QStringList location;
371                   location.append(QString::number(coordinates.latitude()));
372                   location.append(QString::number(coordinates.longitude()));
373                   address = location.join(", ");
374               }
375
376               User buddy = User(address, coordinates, friendMap["name"].toString(),
377                                friendMap["note"].toString(), imageUrl,
378                                friendMap["timestamp"].toString(),
379                                false, friendMap["uid"].toString(), distanceMap["units"].toString(),
380                                distanceMap["value"].toDouble());
381
382               tmpFriendsList.append(buddy);
383             }
384
385             QList<QUrl> imageUrlList; // url list for images
386
387             // set unchanged profile images or add new images to imageUrlList for downloading
388             if(m_user) {
389                 if(m_user->profileImageUrl() != user.profileImageUrl()) {
390                     if(!user.profileImageUrl().isEmpty())
391                         imageUrlList.append(user.profileImageUrl());
392                 } else {
393                     user.setProfileImage(m_user->profileImage());
394                 }
395             } else {
396                 if(!user.profileImageUrl().isEmpty())
397                     imageUrlList.append(user.profileImageUrl());
398             }
399
400             // clear old user object
401             if(m_user) {
402                 delete m_user;
403                 m_user = 0;
404             }
405
406             // create new user object from temporary user object
407             m_user = new User(user);
408
409             // set unchanged profile images or add new images to imageUrlList for downloading
410             if(!m_friendsList.isEmpty()) {
411                 foreach(User tmpBuddy, tmpFriendsList) {
412                     if(!tmpBuddy.profileImageUrl().isEmpty()) {
413                         bool found = false;
414                         foreach(User *buddy, m_friendsList) {
415                             if(tmpBuddy.profileImageUrl() == buddy->profileImageUrl()) {
416                                 tmpBuddy.setProfileImage(buddy->profileImage());
417                                 found = true;
418                                 break;
419                             }
420                         }
421                         if(!found && !tmpBuddy.profileImageUrl().isEmpty())
422                             imageUrlList.append(tmpBuddy.profileImageUrl());
423                     }
424                 }
425             } else {
426                 foreach(User buddy, tmpFriendsList) {
427                     if(!buddy.profileImageUrl().isEmpty())
428                         imageUrlList.append(buddy.profileImageUrl());
429                 }
430             }
431
432             // clear old friendlist
433             qDeleteAll(m_friendsList.begin(), m_friendsList.end());
434             m_friendsList.clear();
435
436             // populate new friendlist with temporary friendlist's data
437             foreach(User tmpFriendItem, tmpFriendsList) {
438                 User *friendItem = new User(tmpFriendItem);
439                 m_friendsList.append(friendItem);
440             }
441             tmpFriendsList.clear();
442
443             emit userDataChanged(m_user, m_friendsList);
444
445             // set silhouette image to imageUrlList for downloading
446             if(m_defaultImage)
447                 imageUrlList.append(QUrl(SILHOUETTE_URL));
448
449             addProfileImages(imageUrlList);
450             imageUrlList.clear();
451         } else {
452             QVariant address = result.value("address");
453             if(!address.toString().isEmpty()) {
454                 emit reverseGeoReady(address.toString());
455             } else {
456                 QStringList coordinates;
457                 coordinates.append(result.value("lat").toString());
458                 coordinates.append(result.value("lon").toString());
459
460                 emit error(ErrorContext::SITUARE, SituareError::ADDRESS_RETRIEVAL_FAILED);
461                 emit reverseGeoReady(coordinates.join(", "));
462             }
463         }
464     }
465 }
466
467 void SituareService::requestFinished(QNetworkReply *reply)
468 {
469     qDebug() << __PRETTY_FUNCTION__;
470
471     //Reply from situare
472     if (m_currentRequests.contains(reply)) {
473
474         qDebug() << "BytesAvailable: " << reply->bytesAvailable();
475
476         if (reply->error()) {
477             emit error(ErrorContext::NETWORK, reply->error());
478         } else {
479             QByteArray replyArray = reply->readAll();
480             qDebug() << "Reply from: " << reply->url() << "reply " << replyArray;
481
482             if(replyArray == ERROR_LAT.toAscii()) {
483                 qDebug() << "Error: " << ERROR_LAT;
484                 emit error(ErrorContext::SITUARE, SituareError::UPDATE_FAILED);
485             } else if(replyArray == ERROR_LON.toAscii()) {
486                 qDebug() << "Error: " << ERROR_LON;
487                 emit error(ErrorContext::SITUARE, SituareError::UPDATE_FAILED);
488             } else if(replyArray.contains(ERROR_SESSION.toAscii())) {
489                 qDebug() << "Error: " << ERROR_SESSION;
490                 emit error(ErrorContext::SITUARE, SituareError::SESSION_EXPIRED);
491             } else if(replyArray.startsWith(OPENING_BRACE_MARK.toAscii())) {
492                 qDebug() << "JSON string";
493                 parseUserData(replyArray);
494             } else if(replyArray.isEmpty()) {
495                 if(reply->url().toString().contains(UPDATE_LOCATION.toAscii())) {
496                     emit updateWasSuccessful();
497                 } else {
498                     // session credentials are invalid
499                     emit error(ErrorContext::SITUARE, SituareError::SESSION_EXPIRED);
500                 }
501             } else {
502                 // unknown reply
503                 emit error(ErrorContext::SITUARE, SituareError::ERROR_GENERAL);
504             }
505         }
506         m_currentRequests.removeAll(reply);
507         reply->deleteLater();
508     }
509 }
510
511 void SituareService::reverseGeo(const GeoCoordinate &coordinates)
512 {
513     qDebug() << __PRETTY_FUNCTION__;
514
515     QHash<QString, QString> parameters;
516     parameters.insert("lat", degreesToString(coordinates.latitude()));
517     parameters.insert("lon", degreesToString(coordinates.longitude()));
518     parameters.insert("format", "json");
519
520     buildRequest(REVERSE_GEO, parameters);
521 }
522
523 void SituareService::sendRequest(const QUrl &url, const QString &cookieType, const QString &cookie)
524 {
525     qDebug() << __PRETTY_FUNCTION__;
526
527     QNetworkRequest request;
528
529     request.setUrl(url);
530     request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
531     request.setRawHeader(cookieType.toAscii(), cookie.toUtf8());
532
533     QNetworkReply *reply = m_networkManager->get(request, true);
534
535     m_currentRequests.append(reply);
536 }
537
538 void SituareService::sendRequest(const QString &requestUrl)
539 {
540     qWarning() << __PRETTY_FUNCTION__ << "requestUrl" << requestUrl;
541
542     // make and send the request
543     QNetworkRequest request;
544     request.setUrl(QUrl(requestUrl));
545     request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
546     QNetworkReply *reply = m_networkManager->get(request, true);
547     m_currentRequests.append(reply);
548 }
549
550 void SituareService::updateSession(const QString &session)
551 {
552     qWarning() << __PRETTY_FUNCTION__;
553
554     m_session = session;
555
556     foreach (QString request, m_requestsWaitingAccessToken) {
557         appendAccessToken(request);
558         sendRequest(request);
559     }
560
561     m_requestsWaitingAccessToken.clear();
562 }
563
564 void SituareService::updateLocation(const GeoCoordinate &coordinates, const QString &status,
565                                     const bool &publish)
566 {
567     qDebug() << __PRETTY_FUNCTION__;
568
569     QHash<QString, QString> parameters;
570     parameters.insert("lat", degreesToString(coordinates.latitude()));
571     parameters.insert("lon", degreesToString(coordinates.longitude()));
572     parameters.insert("publish", publish ? "true" : "false");
573     parameters.insert("data", status); ///< @todo if !empty ???
574
575     buildRequest(UPDATE_LOCATION, parameters);
576 }