1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2018-06-29
7  * Description : a tool to export images to Twitter social network
8  *
9  * Copyright (C) 2018 by Tarek Talaat <tarektalaat93 at gmail dot com>
10  * Copyright (C) 2019 by Thanh Trung Dinh <dinhthanhtrung1996 at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "twittertalker.h"
25 
26 // Qt includes
27 
28 #include <QJsonDocument>
29 #include <QJsonParseError>
30 #include <QJsonObject>
31 #include <QJsonValue>
32 #include <QJsonArray>
33 #include <QByteArray>
34 #include <QUrl>
35 #include <QUrlQuery>
36 #include <QList>
37 #include <QPair>
38 #include <QFileInfo>
39 #include <QWidget>
40 #include <QScopedPointer>
41 #include <QSettings>
42 #include <QMessageBox>
43 #include <QApplication>
44 #include <QDesktopServices>
45 #include <QNetworkAccessManager>
46 
47 /*
48 #include <QWebEngineView>
49 #include <QWebEnginePage>
50 #include <QWebEngineProfile>
51 #include <QWebEngineCookieStore>
52 */
53 
54 // KDE includes
55 
56 #include <klocalizedstring.h>
57 
58 // Local includes
59 
60 #include "digikam_debug.h"
61 #include "digikam_version.h"
62 #include "wstoolutils.h"
63 #include "twitterwindow.h"
64 #include "twitteritem.h"
65 #include "twittermpform.h"
66 #include "previewloadthread.h"
67 #include "o0settingsstore.h"
68 #include "o1requestor.h"
69 
70 namespace DigikamGenericTwitterPlugin
71 {
72 
73 QStringList imageFormat(QString::fromLatin1("jpg,png,gif,webp").split(QLatin1Char(',')));
74 
75 class Q_DECL_HIDDEN TwTalker::Private
76 {
77 public:
78 
79     enum State
80     {
81         TW_USERNAME = 0,
82         TW_LISTFOLDERS,
83         TW_CREATEFOLDER,
84         TW_ADDPHOTO,
85         TW_CREATETWEET,
86         TW_UPLOADINIT,
87         TW_UPLOADAPPEND,
88         TW_UPLOADSTATUSCHECK,
89         TW_UPLOADFINALIZE
90     };
91 
92 public:
93 
Private()94     explicit Private()
95       : clientId        (QLatin1String("lkRgRsucipXsUEvKh0ECblreC")),
96         clientSecret    (QLatin1String("6EThTiPQHZTMo7F83iLHrfNO89fkDVvM9hVWaYH9D49xEOyMBe")),
97         authUrl         (QLatin1String("https://api.twitter.com/oauth/authenticate")),
98         requestTokenUrl (QLatin1String("https://api.twitter.com/oauth/request_token")),
99         accessTokenUrl  (QLatin1String("https://api.twitter.com/oauth/access_token")),
100 /*
101         scope           (QLatin1String("User.Read Files.ReadWrite")),
102 */
103         redirectUrl     (QLatin1String("http://127.0.0.1:8000")),                            // krazy:exclude=insecurenet
104         uploadUrl       (QLatin1String("https://upload.twitter.com/1.1/media/upload.json")),
105         segmentIndex    (0),
106         parent          (nullptr),
107         netMngr         (nullptr),
108         reply           (nullptr),
109         state           (TW_USERNAME),
110         settings        (nullptr),
111         o1Twitter       (nullptr),
112         requestor       (nullptr)
113     {
114     }
115 
116 public:
117 
118     QString                clientId;
119     QString                clientSecret;
120     QString                authUrl;
121     QString                requestTokenUrl;
122     QString                accessTokenUrl;
123     QString                scope;
124     QString                redirectUrl;
125     QString                accessToken;
126     QString                uploadUrl;
127     QString                mediaUploadedPath;
128     QString                mediaId;
129 
130     int                    segmentIndex;
131 
132     QWidget*               parent;
133 
134     QNetworkAccessManager* netMngr;
135 
136     QNetworkReply*         reply;
137 
138     State                  state;
139 
140     QMap<QString, QString> urlParametersMap;
141 /*
142     QWebEngineView*        view;
143 */
144     QSettings*             settings;
145 
146     O1Twitter*             o1Twitter;
147     O1Requestor*           requestor;
148 };
149 
TwTalker(QWidget * const parent)150 TwTalker::TwTalker(QWidget* const parent)
151     : d(new Private)
152 {
153     d->parent  = parent;
154     d->netMngr = new QNetworkAccessManager(this);
155 
156     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
157             this, SLOT(slotFinished(QNetworkReply*)));
158 
159     d->o1Twitter = new O1Twitter(this);
160     d->o1Twitter->setClientId(d->clientId);
161     d->o1Twitter->setClientSecret(d->clientSecret);
162     d->o1Twitter->setLocalPort(8000);
163 
164     d->requestor = new O1Requestor(d->netMngr, d->o1Twitter, this);
165 
166     d->settings                  = WSToolUtils::getOauthSettings(this);
167     O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this);
168     store->setGroupKey(QLatin1String("Twitter"));
169     d->o1Twitter->setStore(store);
170 
171     connect(d->o1Twitter, SIGNAL(linkingFailed()),
172             this, SLOT(slotLinkingFailed()));
173 
174     connect(d->o1Twitter, SIGNAL(linkingSucceeded()),
175             this, SLOT(slotLinkingSucceeded()));
176 
177     connect(d->o1Twitter, SIGNAL(openBrowser(QUrl)),
178             this, SLOT(slotOpenBrowser(QUrl)));
179 }
180 
~TwTalker()181 TwTalker::~TwTalker()
182 {
183     if (d->reply)
184     {
185         d->reply->abort();
186     }
187 
188     WSToolUtils::removeTemporaryDir("twitter");
189 
190     delete d;
191 }
192 
link()193 void TwTalker::link()
194 {
195 /*
196     emit signalBusy(true);
197     QUrl url(d->requestTokenUrl);
198     QNetworkRequest netRequest(url);
199     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
200     netRequest.setRawHeader("Authorization", QString::fromLatin1("OAuth oauth_callback= \"%1\"").arg(d->redirectUrl).toUtf8());
201     QNetworkAccessManager requestMngr;
202     QNetworkReply* reply;
203     reply = requestMngr.post(netRequest);
204 
205     if (reply->error() != QNetworkReply::NoError){
206 
207     }
208 
209     QByteArray buffer;
210     buffer.append(reply->readAll());
211     QString response = fromLatin1(buffer);
212 
213     QMultiMap<QString, QString> headers;
214 
215     // Discard the first line
216     response = response.mid(response.indexOf('\n') + 1).trimmed();
217 
218     foreach (QString line, response.split('\n'))
219     {
220         int colon = line.indexOf(':');
221         QString headerName = line.left(colon).trimmed();
222         QString headerValue = line.mid(colon + 1).trimmed();
223 
224         headers.insert(headerName, headerValue);
225     }
226 
227     QString oauthToken = headers[oauth_token];
228     QSting oauthTokenSecret = headers[oauth_token_secret];
229 
230     QUrlQuery query(url);
231     query.addQueryItem(QLatin1String("client_id"),     d->clientId);
232     query.addQueryItem(QLatin1String("scope"),         d->scope);
233     query.addQueryItem(QLatin1String("redirect_uri"),  d->redirectUrl);
234     query.addQueryItem(QLatin1String("response_type"), "token");
235     url.setQuery(query);
236 
237     d->view = new QWebEngineView(d->parent);
238     d->view->setWindowFlags(Qt::Dialog);
239     d->view->load(url);
240     d->view->show();
241 
242     connect(d->view, SIGNAL(urlChanged(QUrl)),
243             this, SLOT(slotCatchUrl(QUrl)));
244 */
245 
246     emit signalBusy(true);
247     d->o1Twitter->link();
248 }
249 
unLink()250 void TwTalker::unLink()
251 {
252 /*
253     d->accessToken = QString();
254     d->view->page()->profile()->cookieStore()->deleteAllCookies();
255     emit oneDriveLinkingSucceeded();
256 */
257 
258     d->o1Twitter->unlink();
259 
260     d->settings->beginGroup(QLatin1String("Twitter"));
261     d->settings->remove(QString());
262     d->settings->endGroup();
263 }
264 
slotOpenBrowser(const QUrl & url)265 void TwTalker::slotOpenBrowser(const QUrl& url)
266 {
267     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser...";
268     QDesktopServices::openUrl(url);
269 }
270 
ParseUrlParameters(const QString & url)271 QMap<QString, QString> TwTalker::ParseUrlParameters(const QString& url)
272 {
273     QMap<QString, QString> urlParameters;
274 
275     if (url.indexOf(QLatin1Char('?')) == -1)
276     {
277         return urlParameters;
278     }
279 
280     QString tmp           = url.right(url.length()-url.indexOf(QLatin1Char('?'))-1);
281     tmp                   = tmp.right(tmp.length() - tmp.indexOf(QLatin1Char('#'))-1);
282     QStringList paramlist = tmp.split(QLatin1Char('&'));
283 
284     for (int i = 0 ; i < paramlist.count() ; ++i)
285     {
286         QStringList paramarg = paramlist.at(i).split(QLatin1Char('='));
287         urlParameters.insert(paramarg.at(0),paramarg.at(1));
288     }
289 
290     return urlParameters;
291 }
292 
slotLinkingFailed()293 void TwTalker::slotLinkingFailed()
294 {
295     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Twitter fail";
296 
297     emit signalBusy(false);
298 }
299 
slotLinkingSucceeded()300 void TwTalker::slotLinkingSucceeded()
301 {
302     if (!d->o1Twitter->linked())
303     {
304         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Twitter ok";
305 
306         emit signalBusy(false);
307 
308         return;
309     }
310 
311     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Twitter ok";
312     QVariantMap extraTokens = d->o1Twitter->extraTokens();
313 
314     if (!extraTokens.isEmpty())
315     {
316         //emit extraTokensReady(extraTokens);
317 
318         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Extra tokens in response:";
319 
320         foreach (const QString& key, extraTokens.keys())
321         {
322             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "\t"
323                                              << key
324                                              << ":"
325                                              << (extraTokens.value(key).toString().left(3) + QLatin1String("..."));
326         }
327     }
328 
329     emit signalLinkingSucceeded();
330 
331     getUserName();
332 }
333 
authenticated()334 bool TwTalker::authenticated()
335 {
336     return d->o1Twitter->linked();
337 }
338 
cancel()339 void TwTalker::cancel()
340 {
341     if (d->reply)
342     {
343         d->reply->abort();
344         d->reply = nullptr;
345     }
346 
347     emit signalBusy(false);
348 }
349 
addPhoto(const QString & imgPath,const QString &,bool rescale,int maxDim,int imageQuality)350 bool TwTalker::addPhoto(const QString& imgPath,
351                         const QString& /* uploadFolder */,
352                         bool rescale,
353                         int maxDim,
354                         int imageQuality)
355 {
356 
357     QFileInfo imgFileInfo(imgPath);
358     QString path;
359     bool chunked = false;
360 
361     qCDebug(DIGIKAM_WEBSERVICES_LOG) << imgFileInfo.suffix();
362 
363     if ((imgFileInfo.suffix() != QLatin1String("gif")) &&
364         (imgFileInfo.suffix() != QLatin1String("mp4")))
365     {
366         QImage image     = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
367         qint64 imageSize = QFileInfo(imgPath).size();
368         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "SIZE of image using qfileinfo:   " << imageSize;
369         qCDebug(DIGIKAM_WEBSERVICES_LOG) << " ";
370 
371         if (image.isNull())
372         {
373             emit signalBusy(false);
374             return false;
375         }
376 
377         path = WSToolUtils::makeTemporaryDir("twitter").filePath(imgFileInfo.baseName().trimmed() + QLatin1String(".jpg"));
378 
379         if (rescale && ((image.width() > maxDim) || (image.height() > maxDim)))
380         {
381             image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
382         }
383 
384         image.save(path, "JPEG", imageQuality);
385 
386         QScopedPointer<DMetadata> meta(new DMetadata);
387 
388         if (meta->load(imgPath))
389         {
390             meta->setItemDimensions(image.size());
391             meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
392             meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
393             meta->save(path, true);
394         }
395     }
396     else
397     {
398         path = imgPath;
399         chunked = true;
400     }
401 
402     if (chunked)
403     {
404         return addPhotoInit(path);
405     }
406     else
407     {
408         return addPhotoSingleUpload(path);
409     }
410 }
411 
addPhotoSingleUpload(const QString & imgPath)412 bool TwTalker::addPhotoSingleUpload(const QString& imgPath)
413 {
414     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "addPhotoSingleUpload";
415     emit signalBusy(true);
416 
417     TwMPForm form;
418 
419     if (!form.addFile(imgPath))
420     {
421         emit signalBusy(false);
422         return false;
423     }
424 
425     form.finish();
426 
427     if (form.formData().isEmpty())
428     {
429         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Form DATA Empty:";
430     }
431 
432     if (form.formData().isNull())
433     {
434         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Form DATA null:";
435     }
436 
437     QUrl url = QUrl(QLatin1String("https://upload.twitter.com/1.1/media/upload.json"));
438 
439     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
440 
441     QNetworkRequest request(url);
442     request.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
443     d->reply = d->requestor->post(request, reqParams, form.formData());
444 
445     d->state = Private::TW_ADDPHOTO;
446 
447     return true;
448 }
449 
addPhotoInit(const QString & imgPath)450 bool TwTalker::addPhotoInit(const QString& imgPath)
451 {
452     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "addPhotoInit";
453     emit signalBusy(true);
454 
455     TwMPForm form;
456     QByteArray mediaType, mediaCategory;
457     QFileInfo fileInfo(imgPath);
458     QString fileFormat(fileInfo.suffix());
459 
460     form.addPair(form.createPair("command", "INIT"));
461     form.addPair(form.createPair("total_bytes", QString::number(QFileInfo(imgPath).size()).toLatin1()));
462 
463     /* (Feb 2019)
464      * Image file must be <= 5MB
465      * Gif must be <= 15MB
466      * Video must be <= 512MB
467      */
468 
469     if      (imageFormat.indexOf(fileFormat) != -1)
470     {
471         mediaType = "image/jpeg";
472 
473         if (fileFormat == QLatin1String("gif"))
474         {
475 
476             if (fileInfo.size() > 15728640)
477             {
478                 emit signalBusy(false);
479                 emit signalAddPhotoFailed(i18n("File too big to upload"));
480 
481                 return false;
482             }
483 
484             mediaCategory = "TWEET_GIF";
485         }
486         else
487         {
488             if (fileInfo.size() > 5242880)
489             {
490                 emit signalBusy(false);
491                 emit signalAddPhotoFailed(i18n("File too big to upload"));
492 
493                 return false;
494             }
495 
496             mediaCategory = "TWEET_IMAGE";
497         }
498     }
499     else if (fileFormat == QLatin1String("mp4"))
500     {
501         if (fileInfo.size() > 536870912)
502         {
503             emit signalBusy(false);
504             emit signalAddPhotoFailed(i18n("File too big to upload"));
505             return false;
506         }
507 
508         mediaType = "video/mp4";
509         mediaCategory = "TWEET_VIDEO";
510     }
511     else
512     {
513         emit signalBusy(false);
514         emit signalAddPhotoFailed(i18n("Media format is not supported yet"));
515         return false;
516     }
517 
518     form.addPair(form.createPair("media_type", mediaType));
519     form.addPair(form.createPair("media_category", mediaCategory));
520     form.finish();
521 
522     qCDebug(DIGIKAM_WEBSERVICES_LOG) << form.formData();
523 
524     QUrl url(d->uploadUrl);
525 
526     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
527 
528     QNetworkRequest request(url);
529     request.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
530     d->reply = d->requestor->post(request, reqParams, form.formData());
531 
532     d->mediaUploadedPath = imgPath;
533 
534     d->state = Private::TW_UPLOADINIT;
535 
536     return true;
537 }
538 
addPhotoAppend(const QString & mediaId,int segmentIndex)539 bool TwTalker::addPhotoAppend(const QString& mediaId, int segmentIndex)
540 {
541     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "addPhotoAppend: ";
542 
543     static TwMPForm form;
544 
545     if (segmentIndex == 0)
546     {
547         form.addPair(form.createPair("command", "APPEND"));
548         form.addPair(form.createPair("media_id", mediaId.toLatin1()));
549         form.addFile(d->mediaUploadedPath, true);
550         d->segmentIndex = form.numberOfChunks() - 1;
551     }
552     QByteArray data(form.formData());
553     data.append(form.createPair("segment_index", QString::number(segmentIndex).toLatin1()));
554     data.append(form.createPair("media", form.getChunk(segmentIndex)));
555     data.append(form.border());
556 
557     QUrl url(d->uploadUrl);
558     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
559 
560     QNetworkRequest request(url);
561     request.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
562     d->reply = d->requestor->post(request, reqParams, data);
563 
564     d->state = Private::TW_UPLOADAPPEND;
565 
566     // Reset form for later uploads
567 
568     if (segmentIndex == d->segmentIndex)
569     {
570         form.reset();
571     }
572 
573     return true;
574 }
575 
addPhotoFinalize(const QString & mediaId)576 bool TwTalker::addPhotoFinalize(const QString& mediaId)
577 {
578     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "addPhotoFinalize: ";
579 
580     TwMPForm form;
581 
582     form.addPair(form.createPair("command", "FINALIZE"));
583     form.addPair(form.createPair("media_id", mediaId.toLatin1()));
584     form.finish();
585 
586     qCDebug(DIGIKAM_WEBSERVICES_LOG) << form.formData();
587 
588     QUrl url(d->uploadUrl);
589 
590     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
591 
592     QNetworkRequest request(url);
593     request.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
594     d->reply = d->requestor->post(request, reqParams, form.formData());
595 
596     d->state = Private::TW_UPLOADFINALIZE;
597 
598     return true;
599 }
600 
getUserName()601 void TwTalker::getUserName()
602 {
603     /*
604      * The endpoint below allows to get more than just account name (e.g. profile avatar, links to tweets posted, etc.)
605      * Look at debug message printed to console for futher ideas and exploitation
606      */
607     QUrl url(QLatin1String("https://api.twitter.com/1.1/account/verify_credentials.json"));
608 
609     QNetworkRequest request(url);
610     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
611 
612     d->reply = d->requestor->get(request, reqParams);
613     d->state = Private::TW_USERNAME;
614 
615     emit signalBusy(true);
616 }
617 
createTweet(const QString & mediaId)618 void TwTalker::createTweet(const QString& mediaId)
619 {
620     QUrl url = QUrl(QLatin1String("https://api.twitter.com/1.1/statuses/update.json"));
621 
622     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
623     reqParams << O0RequestParameter(QByteArray("status"), QByteArray(""));
624     reqParams << O0RequestParameter(QByteArray("media_ids"), mediaId.toUtf8());
625     QByteArray  postData                = O1::createQueryParameters(reqParams);
626 
627     QNetworkRequest request(url);
628     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM));
629     d->reply = d->requestor->post(request, reqParams, postData);
630 
631     d->state = Private::TW_CREATETWEET;
632 }
633 
slotCheckUploadStatus()634 void TwTalker::slotCheckUploadStatus()
635 {
636     QUrl url = QUrl(d->uploadUrl);
637 
638     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
639     reqParams << O0RequestParameter(QByteArray("command"), QByteArray("STATUS"));
640     reqParams << O0RequestParameter(QByteArray("media_id"), d->mediaId.toUtf8());
641 
642     QUrlQuery query;
643     query.addQueryItem(QLatin1String("command"), QLatin1String("STATUS"));
644     query.addQueryItem(QLatin1String("media_id"), d->mediaId);
645 
646     url.setQuery(query);
647     qCDebug(DIGIKAM_WEBSERVICES_LOG) << url.toString();
648 
649     QNetworkRequest request(url);
650     d->reply = d->requestor->get(request, reqParams);
651     d->state = Private::TW_UPLOADSTATUSCHECK;
652 }
653 
slotFinished(QNetworkReply * reply)654 void TwTalker::slotFinished(QNetworkReply* reply)
655 {
656     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "TwTalker::slotFinished";
657 
658     if (reply != d->reply)
659     {
660         return;
661     }
662 
663     d->reply = nullptr;
664 
665     if (reply->error() != QNetworkReply::NoError)
666     {
667         if (d->state != Private::TW_CREATEFOLDER)
668         {
669             qCDebug(DIGIKAM_WEBSERVICES_LOG) << reply->readAll();
670             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "status code: " << reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
671             emit signalBusy(false);
672             QMessageBox::critical(QApplication::activeWindow(),
673                                   i18n("Error"), reply->errorString());
674 
675             reply->deleteLater();
676             return;
677         }
678     }
679 
680     QByteArray buffer = reply->readAll();
681     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "status code: " << reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
682     static int segmentIndex = 0;
683 
684     switch (d->state)
685     {
686         case Private::TW_LISTFOLDERS:
687             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_LISTFOLDERS";
688             parseResponseListFolders(buffer);
689             break;
690 
691         case Private::TW_CREATEFOLDER:
692             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_CREATEFOLDER";
693             parseResponseCreateFolder(buffer);
694             break;
695 
696         case Private::TW_ADDPHOTO:
697             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_ADDPHOTO";
698             parseResponseAddPhoto(buffer);
699             break;
700 
701         case Private::TW_USERNAME:
702             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_USERNAME";
703             parseResponseUserName(buffer);
704             break;
705 
706         case Private::TW_CREATETWEET:
707             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_CREATETWEET";
708             parseResponseCreateTweet(buffer);
709             break;
710 
711         case Private::TW_UPLOADINIT:
712             segmentIndex = 0;
713             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_UPLOADINIT";
714             parseResponseAddPhotoInit(buffer);
715             break;
716 
717         case Private::TW_UPLOADAPPEND:
718             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_UPLOADAPPEND (at index " << segmentIndex << ")";
719             segmentIndex++;
720             parseResponseAddPhotoAppend(buffer, segmentIndex);
721             break;
722 
723         case Private::TW_UPLOADSTATUSCHECK:
724             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_UPLOADSTATUSCHECK";
725             parseCheckUploadStatus(buffer);
726             break;
727 
728         case Private::TW_UPLOADFINALIZE:
729             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In TW_UPLOADFINALIZE";
730             parseResponseAddPhotoFinalize(buffer);
731             break;
732 
733         default:
734             break;
735     }
736 
737     reply->deleteLater();
738 }
739 
parseResponseAddPhoto(const QByteArray & data)740 void TwTalker::parseResponseAddPhoto(const QByteArray& data)
741 {
742     QJsonParseError err;
743     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
744     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhoto: " << doc;
745 
746     if (err.error != QJsonParseError::NoError)
747     {
748         emit signalBusy(false);
749         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
750         return;
751     }
752 
753     QJsonObject jsonObject = doc.object();
754     QString mediaId        = jsonObject[QLatin1String("media_id_string")].toString();
755     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "media id: " << mediaId;
756 
757     // We haven't emit signalAddPhotoSucceeded() here yet, since we need to update the status first
758 
759     createTweet(mediaId);
760 }
761 
parseResponseAddPhotoInit(const QByteArray & data)762 void TwTalker::parseResponseAddPhotoInit(const QByteArray& data)
763 {
764     QJsonParseError err;
765     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
766     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhotoInit: " << doc;
767 
768     if (err.error != QJsonParseError::NoError)
769     {
770         emit signalBusy(false);
771         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
772         return;
773     }
774 
775     QJsonObject jsonObject = doc.object();
776     d->mediaId             = jsonObject[QLatin1String("media_id_string")].toString();
777     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "media id: " << d->mediaId;
778 
779     // We haven't emit signalAddPhotoSucceeded() here yet, since we need to update the status first
780 
781     addPhotoAppend(d->mediaId);
782 }
783 
parseResponseAddPhotoAppend(const QByteArray &,int segmentIndex)784 void TwTalker::parseResponseAddPhotoAppend(const QByteArray& /*data*/, int segmentIndex)
785 {
786     /* (Fev. 2019)
787      * Currently, we don't parse data of response from addPhotoAppend, since the response is with HTTP 204
788      * This is indeed an expected response code, because the response should be with an empty body.
789      * However, in order to keep a compatible prototype of parseResponse methodes and reserve for future change,
790      * we should keep argument const QByteArray& data.
791      */
792     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhotoAppend: ";
793 
794     if (segmentIndex <= d->segmentIndex)
795     {
796         addPhotoAppend(d->mediaId, segmentIndex);
797     }
798     else
799     {
800         addPhotoFinalize(d->mediaId);
801     }
802 }
803 
parseResponseAddPhotoFinalize(const QByteArray & data)804 void TwTalker::parseResponseAddPhotoFinalize(const QByteArray& data)
805 {
806     QJsonParseError err;
807     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
808     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhotoFinalize: " << doc;
809 
810     if (err.error != QJsonParseError::NoError)
811     {
812         emit signalBusy(false);
813         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
814 
815         return;
816     }
817 
818     QJsonObject jsonObject    = doc.object();
819     QJsonValue processingInfo = jsonObject[QLatin1String("processing_info")];
820 
821     if (processingInfo != QJsonValue::Undefined)
822     {
823         QString state = processingInfo.toObject()[QLatin1String("state")].toString();
824         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "state: " << state;
825 
826         if (state == QLatin1String("pending"))
827         {
828             QTimer::singleShot(processingInfo.toObject()[QLatin1String("check_after_secs")].toInt()*1000 /*msec*/,
829                                this, SLOT(slotCheckUploadStatus()));
830         }
831     }
832     else
833     {
834         // We haven't emit signalAddPhotoSucceeded() here yet, since we need to update the status first
835 
836         createTweet(d->mediaId);
837     }
838 }
839 
parseCheckUploadStatus(const QByteArray & data)840 void TwTalker::parseCheckUploadStatus(const QByteArray& data)
841 {
842     QJsonParseError err;
843     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
844     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseCheckUploadStatus: " << doc;
845 
846     if (err.error != QJsonParseError::NoError)
847     {
848         emit signalBusy(false);
849         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
850         return;
851     }
852 
853     QJsonObject jsonObject     = doc.object();
854     QJsonObject processingInfo = jsonObject[QLatin1String("processing_info")].toObject();
855     QString state              = processingInfo[QLatin1String("state")].toString();
856 
857     if      (state == QLatin1String("in_progress"))
858     {
859         QTimer::singleShot(processingInfo[QLatin1String("check_after_secs")].toInt()*1000 /*msec*/, this, SLOT(slotCheckUploadStatus()));
860     }
861     else if (state == QLatin1String("failed"))
862     {
863         QJsonObject error = processingInfo[QLatin1String("error")].toObject();
864         emit signalBusy(false);
865         emit signalAddPhotoFailed(i18n("Failed to upload photo\n"
866                                        "Code: %1, name: %2, message: %3",
867                                        error[QLatin1String("code")].toInt(),
868                                        error[QLatin1String("name")].toString(),
869                                        error[QLatin1String("message")].toString()));
870         return;
871     }
872     else // succeeded
873     {
874         // We haven't emit signalAddPhotoSucceeded() here yet, since we need to update the status first
875 
876         createTweet(d->mediaId);
877     }
878 }
879 
parseResponseUserName(const QByteArray & data)880 void TwTalker::parseResponseUserName(const QByteArray& data)
881 {
882     QJsonParseError err;
883     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
884     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseUserName: "<<doc;
885 
886     if (err.error != QJsonParseError::NoError)
887     {
888         emit signalBusy(false);
889         return;
890     }
891 
892     QJsonObject obj     = doc.object();
893     QString name        = obj[QLatin1String("name")].toString();
894     QString screenName  = obj[QLatin1String("screen_name")].toString();
895     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "user full name: "<<name;
896     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "user screen name: @" << screenName;
897 
898     emit signalBusy(false);
899     emit signalSetUserName(QString::fromLatin1("%1 (@%2)").arg(name).arg(screenName));
900 }
901 
parseResponseCreateTweet(const QByteArray & data)902 void TwTalker::parseResponseCreateTweet(const QByteArray& data)
903 {
904     QJsonParseError err;
905     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
906     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseCreateTweet: " << doc;
907 
908     if (err.error != QJsonParseError::NoError)
909     {
910         emit signalBusy(false);
911         emit signalAddPhotoFailed(i18n("Failed to create tweet for photo uploaded"));
912 
913         return;
914     }
915 
916     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Tweet posted successfully!";
917     emit signalBusy(false);
918     emit signalAddPhotoSucceeded();
919 }
920 
parseResponseListFolders(const QByteArray & data)921 void TwTalker::parseResponseListFolders(const QByteArray& data)
922 {
923     QJsonParseError err;
924     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
925 
926     if (err.error != QJsonParseError::NoError)
927     {
928         emit signalBusy(false);
929         emit signalListAlbumsFailed(i18n("Failed to list folders"));
930         return;
931     }
932 
933     QJsonObject jsonObject = doc.object();
934 
935     //qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Json: " << doc;
936 
937     QJsonArray jsonArray   = jsonObject[QLatin1String("value")].toArray();
938 
939     //qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Json response: " << jsonArray;
940 
941     QList<QPair<QString, QString> > list;
942     list.append(qMakePair(QLatin1String(""), QLatin1String("root")));
943 
944     foreach (const QJsonValue& value, jsonArray)
945     {
946         QString path;
947         QString folderName;
948         QJsonObject folder;
949 
950         QJsonObject obj = value.toObject();
951         folder          = obj[QLatin1String("folder")].toObject();
952 
953         if (!folder.isEmpty())
954         {
955             folderName    = obj[QLatin1String("name")].toString();
956             path          = QLatin1Char('/') + folderName;
957             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Folder Name is" << folderName;
958             list.append(qMakePair(path, folderName));
959         }
960     }
961 
962     emit signalBusy(false);
963     emit signalListAlbumsDone(list);
964 }
965 
parseResponseCreateFolder(const QByteArray & data)966 void TwTalker::parseResponseCreateFolder(const QByteArray& data)
967 {
968     QJsonDocument doc1      = QJsonDocument::fromJson(data);
969     QJsonObject jsonObject = doc1.object();
970     bool fail              = jsonObject.contains(QLatin1String("error"));
971 
972     emit signalBusy(false);
973 
974     if (fail)
975     {
976       QJsonParseError err;
977       QJsonDocument doc2 = QJsonDocument::fromJson(data, &err);
978       qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseCreateFolder ERROR: " << doc2;
979 
980       emit signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString());
981     }
982     else
983     {
984         emit signalCreateFolderSucceeded();
985     }
986 }
987 
988 } // namespace DigikamGenericTwitterPlugin
989