1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2018-05-20
7 * Description : a tool to export images to Pinterest web service
8 *
9 * Copyright (C) 2018 by Tarek Talaat <tarektalaat93 at gmail dot com>
10 *
11 * This program is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU General
13 * Public License as published by the Free Software Foundation;
14 * either version 2, or (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * ============================================================ */
22
23 #include "ptalker.h"
24
25 // Qt includes
26
27 #include <QJsonDocument>
28 #include <QJsonParseError>
29 #include <QJsonObject>
30 #include <QJsonValue>
31 #include <QJsonArray>
32 #include <QByteArray>
33 #include <QList>
34 #include <QPair>
35 #include <QFileInfo>
36 #include <QWidget>
37 #include <QMessageBox>
38 #include <QApplication>
39 #include <QDesktopServices>
40 #include <QUrlQuery>
41 #include <QHttpMultiPart>
42 #include <QNetworkAccessManager>
43 #include <QScopedPointer>
44
45 // KDE includes
46
47 #include <klocalizedstring.h>
48 #include <kwindowconfig.h>
49
50 // Local includes
51
52 #include "digikam_debug.h"
53 #include "digikam_version.h"
54 #include "wstoolutils.h"
55 #include "pwindow.h"
56 #include "pitem.h"
57 #include "webbrowserdlg.h"
58 #include "previewloadthread.h"
59
60 namespace DigikamGenericPinterestPlugin
61 {
62
63 class Q_DECL_HIDDEN PTalker::Private
64 {
65 public:
66
67 enum State
68 {
69 P_USERNAME = 0,
70 P_LISTBOARDS,
71 P_CREATEBOARD,
72 P_ADDPIN,
73 P_ACCESSTOKEN
74 };
75
76 public:
77
Private()78 explicit Private()
79 : parent (nullptr),
80 netMngr (nullptr),
81 reply (nullptr),
82 settings(nullptr),
83 state (P_USERNAME),
84 browser (nullptr)
85 {
86 clientId = QLatin1String("4983380570301022071");
87 clientSecret = QLatin1String("2a698db679125930d922a2dfb897e16b668a67c6f614593636e83fc3d8d9b47d");
88
89 authUrl = QLatin1String("https://api.pinterest.com/oauth/");
90 tokenUrl = QLatin1String("https://api.pinterest.com/v1/oauth/token");
91 redirectUrl = QLatin1String("https://login.live.com/oauth20_desktop.srf");
92 scope = QLatin1String("read_public,write_public");
93 serviceName = QLatin1String("Pinterest");
94 serviceKey = QLatin1String("access_token");
95 }
96
97 public:
98
99 QString clientId;
100 QString clientSecret;
101 QString authUrl;
102 QString tokenUrl;
103 QString redirectUrl;
104 QString accessToken;
105 QString scope;
106 QString userName;
107 QString serviceName;
108 QString serviceKey;
109
110 QWidget* parent;
111
112 QNetworkAccessManager* netMngr;
113 QNetworkReply* reply;
114
115 QSettings* settings;
116
117 State state;
118
119 QMap<QString, QString> urlParametersMap;
120
121 WebBrowserDlg* browser;
122 };
123
PTalker(QWidget * const parent)124 PTalker::PTalker(QWidget* const parent)
125 : d(new Private)
126 {
127 d->parent = parent;
128 d->netMngr = new QNetworkAccessManager(this);
129 d->settings = WSToolUtils::getOauthSettings(this);
130
131 connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
132 this, SLOT(slotFinished(QNetworkReply*)));
133
134 connect(this, SIGNAL(pinterestLinkingFailed()),
135 this, SLOT(slotLinkingFailed()));
136
137 connect(this, SIGNAL(pinterestLinkingSucceeded()),
138 this, SLOT(slotLinkingSucceeded()));
139 }
140
~PTalker()141 PTalker::~PTalker()
142 {
143 if (d->reply)
144 {
145 d->reply->abort();
146 }
147
148 WSToolUtils::removeTemporaryDir("pinterest");
149
150 delete d;
151 }
152
link()153 void PTalker::link()
154 {
155 emit signalBusy(true);
156
157 QUrl url(d->authUrl);
158 QUrlQuery query(url);
159 query.addQueryItem(QLatin1String("client_id"), d->clientId);
160 query.addQueryItem(QLatin1String("scope"), d->scope);
161 query.addQueryItem(QLatin1String("redirect_uri"), d->redirectUrl);
162 query.addQueryItem(QLatin1String("response_type"), QLatin1String("code"));
163 url.setQuery(query);
164
165 delete d->browser;
166 d->browser = new WebBrowserDlg(url, d->parent, true);
167 d->browser->setModal(true);
168
169 connect(d->browser, SIGNAL(urlChanged(QUrl)),
170 this, SLOT(slotCatchUrl(QUrl)));
171
172 connect(d->browser, SIGNAL(closeView(bool)),
173 this, SIGNAL(signalBusy(bool)));
174
175 d->browser->show();
176 }
177
unLink()178 void PTalker::unLink()
179 {
180 d->accessToken = QString();
181
182 d->settings->beginGroup(d->serviceName);
183 d->settings->remove(QString());
184 d->settings->endGroup();
185
186 emit pinterestLinkingSucceeded();
187 }
188
slotCatchUrl(const QUrl & url)189 void PTalker::slotCatchUrl(const QUrl& url)
190 {
191 d->urlParametersMap = ParseUrlParameters(url.toString());
192 QString code = d->urlParametersMap.value(QLatin1String("code"));
193 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received URL from webview in link function: " << url ;
194
195 if (!code.isEmpty())
196 {
197 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "CODE Received";
198 d->browser->close();
199 getToken(code);
200 emit signalBusy(false);
201 }
202 }
203
getToken(const QString & code)204 void PTalker::getToken(const QString& code)
205 {
206 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Code: " << code;
207 QUrl url(d->tokenUrl);
208 QUrlQuery query(url);
209 query.addQueryItem(QLatin1String("grant_type"), QLatin1String("authorization_code"));
210 query.addQueryItem(QLatin1String("client_id"), d->clientId);
211 query.addQueryItem(QLatin1String("client_secret"), d->clientSecret);
212 query.addQueryItem(QLatin1String("code"), code);
213 url.setQuery(query);
214 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Token Request URL: " << url.toString();
215
216 QNetworkRequest netRequest(url);
217 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
218 netRequest.setRawHeader("Accept", "application/json");
219
220 d->reply = d->netMngr->post(netRequest, QByteArray());
221
222 d->state = Private::P_ACCESSTOKEN;
223 }
224
ParseUrlParameters(const QString & url)225 QMap<QString, QString> PTalker::ParseUrlParameters(const QString& url)
226 {
227 QMap<QString, QString> urlParameters;
228
229 if (url.indexOf(QLatin1Char('?')) == -1)
230 {
231 return urlParameters;
232 }
233
234 QString tmp = url.right(url.length()-url.indexOf(QLatin1Char('?')) - 1);
235 QStringList paramlist = tmp.split(QLatin1Char('&'));
236
237 for (int i = 0 ; i < paramlist.count() ; ++i)
238 {
239 QStringList paramarg = paramlist.at(i).split(QLatin1Char('='));
240
241 if (paramarg.count() == 2)
242 {
243 urlParameters.insert(paramarg.at(0), paramarg.at(1));
244 }
245 }
246
247 return urlParameters;
248 }
249
slotLinkingFailed()250 void PTalker::slotLinkingFailed()
251 {
252 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Pinterest fail";
253 emit signalBusy(false);
254 }
255
slotLinkingSucceeded()256 void PTalker::slotLinkingSucceeded()
257 {
258 if (d->accessToken.isEmpty())
259 {
260 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Pinterest ok";
261 emit signalBusy(false);
262 return;
263 }
264
265 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Pinterest ok";
266 writeSettings();
267 emit signalLinkingSucceeded();
268 }
269
authenticated()270 bool PTalker::authenticated()
271 {
272 return (!d->accessToken.isEmpty());
273 }
274
cancel()275 void PTalker::cancel()
276 {
277 if (d->reply)
278 {
279 d->reply->abort();
280 d->reply = nullptr;
281 }
282
283 emit signalBusy(false);
284 }
285
createBoard(QString & boardName)286 void PTalker::createBoard(QString& boardName)
287 {
288 QUrl url(QLatin1String("https://api.pinterest.com/v1/boards/"));
289 QNetworkRequest netRequest(url);
290 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
291 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
292
293 QByteArray postData = QString::fromUtf8("{\"name\": \"%1\"}").arg(boardName).toUtf8();
294 /*
295 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "createBoard:" << postData;
296 */
297 d->reply = d->netMngr->post(netRequest, postData);
298
299 d->state = Private::P_CREATEBOARD;
300 emit signalBusy(true);
301 }
302
getUserName()303 void PTalker::getUserName()
304 {
305 QUrl url(QLatin1String("https://api.pinterest.com/v1/me/?fields=username"));
306
307 QNetworkRequest netRequest(url);
308 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
309
310 d->reply = d->netMngr->get(netRequest);
311 d->state = Private::P_USERNAME;
312 emit signalBusy(true);
313 }
314
315 /**
316 * Get list of boards by parsing json sent by pinterest
317 */
listBoards(const QString &)318 void PTalker::listBoards(const QString& /*path*/)
319 {
320 QUrl url(QLatin1String("https://api.pinterest.com/v1/me/boards/"));;
321
322 QNetworkRequest netRequest(url);
323 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
324 /*
325 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
326 */
327 d->reply = d->netMngr->get(netRequest);
328
329 d->state = Private::P_LISTBOARDS;
330 emit signalBusy(true);
331 }
332
addPin(const QString & imgPath,const QString & uploadBoard,bool rescale,int maxDim,int imageQuality)333 bool PTalker::addPin(const QString& imgPath,
334 const QString& uploadBoard,
335 bool rescale,
336 int maxDim,
337 int imageQuality)
338 {
339 if (d->reply)
340 {
341 d->reply->abort();
342 d->reply = nullptr;
343 }
344
345 emit signalBusy(true);
346
347 QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
348
349 if (image.isNull())
350 {
351 emit signalBusy(false);
352 return false;
353 }
354
355 QString path = WSToolUtils::makeTemporaryDir("pinterest").filePath(QFileInfo(imgPath)
356 .baseName().trimmed() + QLatin1String(".jpg"));
357
358 if (rescale && ((image.width() > maxDim) || (image.height() > maxDim)))
359 {
360 image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
361 }
362
363 image.save(path, "JPEG", imageQuality);
364
365 QScopedPointer<DMetadata> meta(new DMetadata);
366
367 if (meta->load(imgPath))
368 {
369 meta->setItemDimensions(image.size());
370 meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
371 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
372 meta->save(path, true);
373 }
374
375 QString boardParam = d->userName + QLatin1Char('/') + uploadBoard;
376
377 QUrl url(QString::fromLatin1("https://api.pinterest.com/v1/pins/?access_token=%1").arg(d->accessToken));
378
379 QHttpMultiPart* const multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
380
381 // Board Section
382
383 QHttpPart board;
384 QString boardHeader = QLatin1String("form-data; name=\"board\"") ;
385 board.setHeader(QNetworkRequest::ContentDispositionHeader, boardHeader);
386
387 QByteArray postData = boardParam.toUtf8();
388 board.setBody(postData);
389 multiPart->append(board);
390
391 // Note section
392
393 QHttpPart note;
394 QString noteHeader = QLatin1String("form-data; name=\"note\"") ;
395 note.setHeader(QNetworkRequest::ContentDispositionHeader, noteHeader);
396
397 postData = QByteArray();
398
399 note.setBody(postData);
400 multiPart->append(note);
401
402 // image section
403
404 QFile* const file = new QFile(imgPath);
405
406 if (!file)
407 {
408 return false;
409 }
410
411 if (!file->open(QIODevice::ReadOnly))
412 {
413 return false;
414 }
415
416 QHttpPart imagePart;
417 QString imagePartHeader = QLatin1String("form-data; name=\"image\"; filename=\"") +
418 QFileInfo(imgPath).fileName() + QLatin1Char('"');
419
420 imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imagePartHeader);
421 imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("image/jpeg"));
422
423 imagePart.setBodyDevice(file);
424 multiPart->append(imagePart);
425
426 QString content = QLatin1String("multipart/form-data;boundary=") + QString::fromUtf8(multiPart->boundary());
427 QNetworkRequest netRequest(url);
428 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, content);
429
430 d->reply = d->netMngr->post(netRequest, multiPart);
431
432 // delete the multiPart and file with the reply
433
434 multiPart->setParent(d->reply);
435 d->state = Private::P_ADDPIN;
436
437 return true;
438 }
439
slotFinished(QNetworkReply * reply)440 void PTalker::slotFinished(QNetworkReply* reply)
441 {
442 if (reply != d->reply)
443 {
444 return;
445 }
446
447 d->reply = nullptr;
448
449 if (reply->error() != QNetworkReply::NoError)
450 {
451 if (d->state != Private::P_CREATEBOARD)
452 {
453 emit signalBusy(false);
454 QMessageBox::critical(QApplication::activeWindow(),
455 i18n("Error"), reply->errorString());
456 /*
457 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error content: " << QString(reply->readAll());
458 */
459 reply->deleteLater();
460 return;
461 }
462 }
463
464 QByteArray buffer = reply->readAll();
465 /*
466 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "BUFFER" << QString(buffer);
467 */
468 switch (d->state)
469 {
470 case Private::P_LISTBOARDS:
471 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_LISTBOARDS";
472 parseResponseListBoards(buffer);
473 break;
474
475 case Private::P_CREATEBOARD:
476 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_CREATEBOARD";
477 parseResponseCreateBoard(buffer);
478 break;
479
480 case Private::P_ADDPIN:
481 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_ADDPIN";
482 parseResponseAddPin(buffer);
483 break;
484
485 case Private::P_USERNAME:
486 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_USERNAME";
487 parseResponseUserName(buffer);
488 break;
489
490 case Private::P_ACCESSTOKEN:
491 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_ACCESSTOKEN";
492 parseResponseAccessToken(buffer);
493 break;
494
495 default:
496 break;
497 }
498
499 reply->deleteLater();
500 }
501
parseResponseAccessToken(const QByteArray & data)502 void PTalker::parseResponseAccessToken(const QByteArray& data)
503 {
504 QJsonDocument doc = QJsonDocument::fromJson(data);
505 QJsonObject jsonObject = doc.object();
506 d->accessToken = jsonObject[QLatin1String("access_token")].toString();
507
508 if (!d->accessToken.isEmpty())
509 {
510 qDebug(DIGIKAM_WEBSERVICES_LOG) << "Access token Received: " << d->accessToken;
511 emit pinterestLinkingSucceeded();
512 }
513 else
514 {
515 emit pinterestLinkingFailed();
516 }
517
518 emit signalBusy(false);
519 }
520
parseResponseAddPin(const QByteArray & data)521 void PTalker::parseResponseAddPin(const QByteArray& data)
522 {
523 QJsonDocument doc = QJsonDocument::fromJson(data);
524 QJsonObject jsonObject = doc.object()[QLatin1String("data")].toObject();
525 bool success = jsonObject.contains(QLatin1String("id"));
526 emit signalBusy(false);
527
528 if (!success)
529 {
530 emit signalAddPinFailed(i18n("Failed to upload Pin"));
531 }
532 else
533 {
534 emit signalAddPinSucceeded();
535 }
536 }
537
parseResponseUserName(const QByteArray & data)538 void PTalker::parseResponseUserName(const QByteArray& data)
539 {
540 QJsonDocument doc = QJsonDocument::fromJson(data);
541 QJsonObject jsonObject = doc.object()[QLatin1String("data")].toObject();
542 d->userName = jsonObject[QLatin1String("username")].toString();
543
544 emit signalBusy(false);
545 emit signalSetUserName(d->userName);
546 }
547
parseResponseListBoards(const QByteArray & data)548 void PTalker::parseResponseListBoards(const QByteArray& data)
549 {
550 QJsonParseError err;
551 QJsonDocument doc = QJsonDocument::fromJson(data, &err);
552
553 if (err.error != QJsonParseError::NoError)
554 {
555 emit signalBusy(false);
556 emit signalListBoardsFailed(i18n("Failed to list boards"));
557 return;
558 }
559
560 QJsonObject jsonObject = doc.object();
561 /*
562 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Json Listing Boards : " << doc;
563 */
564 QJsonArray jsonArray = jsonObject[QLatin1String("data")].toArray();
565
566 QList<QPair<QString, QString> > list;
567
568 foreach (const QJsonValue& value, jsonArray)
569 {
570 QString boardID;
571 QString boardName;
572 QJsonObject obj = value.toObject();
573 boardID = obj[QLatin1String("id")].toString();
574 boardName = obj[QLatin1String("name")].toString();
575
576 list.append(qMakePair(boardID, boardName));
577 }
578
579 emit signalBusy(false);
580 emit signalListBoardsDone(list);
581 }
582
parseResponseCreateBoard(const QByteArray & data)583 void PTalker::parseResponseCreateBoard(const QByteArray& data)
584 {
585 QJsonDocument doc1 = QJsonDocument::fromJson(data);
586 QJsonObject jsonObject = doc1.object();
587 bool fail = jsonObject.contains(QLatin1String("error"));
588
589 emit signalBusy(false);
590
591 if (fail)
592 {
593 QJsonParseError err;
594 QJsonDocument doc2 = QJsonDocument::fromJson(data, &err);
595 emit signalCreateBoardFailed(jsonObject[QLatin1String("error_summary")].toString());
596 }
597 else
598 {
599 emit signalCreateBoardSucceeded();
600 }
601 }
602
writeSettings()603 void PTalker::writeSettings()
604 {
605 d->settings->beginGroup(d->serviceName);
606 d->settings->setValue(d->serviceKey, d->accessToken);
607 d->settings->endGroup();
608 }
609
readSettings()610 void PTalker::readSettings()
611 {
612 d->settings->beginGroup(d->serviceName);
613 d->accessToken = d->settings->value(d->serviceKey).toString();
614 d->settings->endGroup();
615
616 if (d->accessToken.isEmpty())
617 {
618 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Linking...";
619 link();
620 }
621 else
622 {
623 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Already Linked";
624 emit pinterestLinkingSucceeded();
625 }
626 }
627
628 } // namespace DigikamGenericPinterestPlugin
629