File indexing completed on 2025-01-19 03:53:10

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2018-05-20
0007  * Description : a tool to export images to Pinterest web service
0008  *
0009  * SPDX-FileCopyrightText: 2018      by Tarek Talaat <tarektalaat93 at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "ptalker.h"
0016 
0017 // Qt includes
0018 
0019 #include <QJsonDocument>
0020 #include <QJsonParseError>
0021 #include <QJsonObject>
0022 #include <QJsonValue>
0023 #include <QJsonArray>
0024 #include <QByteArray>
0025 #include <QFileInfo>
0026 #include <QWidget>
0027 #include <QMessageBox>
0028 #include <QApplication>
0029 #include <QUrlQuery>
0030 #include <QHttpMultiPart>
0031 #include <QScopedPointer>
0032 
0033 // KDE includes
0034 
0035 #include <klocalizedstring.h>
0036 #include <kwindowconfig.h>
0037 
0038 // Local includes
0039 
0040 #include "digikam_debug.h"
0041 #include "digikam_version.h"
0042 #include "previewloadthread.h"
0043 #include "networkmanager.h"
0044 #include "webbrowserdlg.h"
0045 #include "wstoolutils.h"
0046 #include "pwindow.h"
0047 
0048 namespace DigikamGenericPinterestPlugin
0049 {
0050 
0051 class Q_DECL_HIDDEN PTalker::Private
0052 {
0053 public:
0054 
0055     enum State
0056     {
0057         P_USERNAME = 0,
0058         P_LISTBOARDS,
0059         P_CREATEBOARD,
0060         P_ADDPIN,
0061         P_ACCESSTOKEN
0062     };
0063 
0064 public:
0065 
0066     explicit Private()
0067       : parent  (nullptr),
0068         netMngr (nullptr),
0069         reply   (nullptr),
0070         settings(nullptr),
0071         state   (P_USERNAME),
0072         browser (nullptr)
0073     {
0074         clientId     = QLatin1String("1477112");
0075         clientSecret = QLatin1String("69dc00477dd1c59430b15675d92ff30136126dcb");
0076 
0077         authUrl      = QLatin1String("https://www.pinterest.com/oauth/");
0078         tokenUrl     = QLatin1String("https://api.pinterest.com/v5/oauth/token");
0079         redirectUrl  = QLatin1String("https://login.live.com/oauth20_desktop.srf");
0080         scope        = QLatin1String("boards:read,boards:write,pins:read,pins:write,user_accounts:read");
0081         serviceName  = QLatin1String("Pinterest");
0082         serviceKey   = QLatin1String("access_token");
0083     }
0084 
0085 public:
0086 
0087     QString                clientId;
0088     QString                clientSecret;
0089     QString                authUrl;
0090     QString                tokenUrl;
0091     QString                redirectUrl;
0092     QString                accessToken;
0093     QString                scope;
0094     QString                userName;
0095     QString                serviceName;
0096     QString                serviceKey;
0097 
0098     QWidget*               parent;
0099 
0100     QNetworkAccessManager* netMngr;
0101     QNetworkReply*         reply;
0102 
0103     QSettings*             settings;
0104 
0105     State                  state;
0106 
0107     QMap<QString, QString> urlParametersMap;
0108 
0109     WebBrowserDlg*         browser;
0110 };
0111 
0112 PTalker::PTalker(QWidget* const parent)
0113     : d(new Private)
0114 {
0115     d->parent   = parent;
0116     d->netMngr  = NetworkManager::instance()->getNetworkManager(this);
0117     d->settings = WSToolUtils::getOauthSettings(this);
0118 
0119     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
0120             this, SLOT(slotFinished(QNetworkReply*)));
0121 
0122     connect(this, SIGNAL(pinterestLinkingFailed()),
0123             this, SLOT(slotLinkingFailed()));
0124 
0125     connect(this, SIGNAL(pinterestLinkingSucceeded()),
0126             this, SLOT(slotLinkingSucceeded()));
0127 }
0128 
0129 PTalker::~PTalker()
0130 {
0131     if (d->reply)
0132     {
0133         d->reply->abort();
0134     }
0135 
0136     WSToolUtils::removeTemporaryDir("pinterest");
0137 
0138     delete d;
0139 }
0140 
0141 void PTalker::link()
0142 {
0143     Q_EMIT signalBusy(true);
0144 
0145     QUrl url(d->authUrl);
0146     QUrlQuery query(url);
0147     query.addQueryItem(QLatin1String("client_id"),     d->clientId);
0148     query.addQueryItem(QLatin1String("scope"),         d->scope);
0149     query.addQueryItem(QLatin1String("redirect_uri"),  d->redirectUrl);
0150     query.addQueryItem(QLatin1String("response_type"), QLatin1String("code"));
0151     url.setQuery(query);
0152 
0153     d->browser = new WebBrowserDlg(url, d->parent, true);
0154     d->browser->setModal(true);
0155 
0156     connect(d->browser, SIGNAL(urlChanged(QUrl)),
0157             this, SLOT(slotCatchUrl(QUrl)));
0158 
0159     connect(d->browser, SIGNAL(closeView(bool)),
0160             this, SIGNAL(signalBusy(bool)));
0161 
0162     d->browser->show();
0163 }
0164 
0165 void PTalker::unLink()
0166 {
0167     d->accessToken = QString();
0168 
0169     d->settings->beginGroup(d->serviceName);
0170     d->settings->remove(QString());
0171     d->settings->endGroup();
0172 
0173     Q_EMIT pinterestLinkingSucceeded();
0174 }
0175 
0176 void PTalker::slotCatchUrl(const QUrl& url)
0177 {
0178     d->urlParametersMap = ParseUrlParameters(url.toString());
0179     QString code        = d->urlParametersMap.value(QLatin1String("code"));
0180     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received URL from webview in link function:" << url ;
0181 
0182     if (!code.isEmpty())
0183     {
0184         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "CODE Received";
0185         d->browser->close();
0186         getToken(code);
0187         Q_EMIT signalBusy(false);
0188     }
0189 }
0190 
0191 void PTalker::getToken(const QString& code)
0192 {
0193     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Code:" << code;
0194 
0195     QUrlQuery query;
0196     query.addQueryItem(QLatin1String("grant_type"),   QLatin1String("authorization_code"));
0197     query.addQueryItem(QLatin1String("redirect_uri"), d->redirectUrl);
0198     query.addQueryItem(QLatin1String("code"),         code);
0199 
0200     QByteArray basic = d->clientId.toLatin1() + QByteArray(":") + d->clientSecret.toLatin1();
0201     basic            = basic.toBase64();
0202 
0203     QNetworkRequest netRequest(QUrl(d->tokenUrl));
0204     netRequest.setRawHeader("Authorization", QString::fromLatin1("Basic %1").arg(QLatin1String(basic)).toLatin1());
0205     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
0206     netRequest.setRawHeader("Accept", "application/json");
0207 
0208     d->reply = d->netMngr->post(netRequest, query.toString().toLatin1());
0209 
0210     d->state = Private::P_ACCESSTOKEN;
0211 }
0212 
0213 QMap<QString, QString> PTalker::ParseUrlParameters(const QString& url)
0214 {
0215     QMap<QString, QString> urlParameters;
0216 
0217     if (url.indexOf(QLatin1Char('?')) == -1)
0218     {
0219         return urlParameters;
0220     }
0221 
0222     QString tmp           = url.right(url.length()-url.indexOf(QLatin1Char('?')) - 1);
0223     QStringList paramlist = tmp.split(QLatin1Char('&'));
0224 
0225     for (int i = 0 ; i < paramlist.count() ; ++i)
0226     {
0227         QStringList paramarg = paramlist.at(i).split(QLatin1Char('='));
0228 
0229         if (paramarg.count() == 2)
0230         {
0231             urlParameters.insert(paramarg.at(0), paramarg.at(1));
0232         }
0233     }
0234 
0235     return urlParameters;
0236 }
0237 
0238 void PTalker::slotLinkingFailed()
0239 {
0240     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Pinterest fail";
0241     Q_EMIT signalBusy(false);
0242 }
0243 
0244 void PTalker::slotLinkingSucceeded()
0245 {
0246     if (d->accessToken.isEmpty())
0247     {
0248         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Pinterest ok";
0249         Q_EMIT signalBusy(false);
0250         return;
0251     }
0252 
0253     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Pinterest ok";
0254     writeSettings();
0255     Q_EMIT signalLinkingSucceeded();
0256 }
0257 
0258 bool PTalker::authenticated()
0259 {
0260     return (!d->accessToken.isEmpty());
0261 }
0262 
0263 void PTalker::cancel()
0264 {
0265     if (d->reply)
0266     {
0267         d->reply->abort();
0268         d->reply = nullptr;
0269     }
0270 
0271     Q_EMIT signalBusy(false);
0272 }
0273 
0274 void PTalker::createBoard(QString& boardName)
0275 {
0276     QUrl url(QLatin1String("https://api.pinterest.com/v5/boards"));
0277     QNetworkRequest netRequest(url);
0278     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0279     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0280 
0281     QByteArray postData = QString::fromUtf8("{\"name\": \"%1\"}").arg(boardName).toUtf8();
0282 /*
0283     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "createBoard:" << postData;
0284 */
0285     d->reply = d->netMngr->post(netRequest, postData);
0286 
0287     d->state = Private::P_CREATEBOARD;
0288     Q_EMIT signalBusy(true);
0289 }
0290 
0291 void PTalker::getUserName()
0292 {
0293     QUrl url(QLatin1String("https://api.pinterest.com/v5/user_account"));
0294 
0295     QNetworkRequest netRequest(url);
0296     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0297 
0298     d->reply = d->netMngr->get(netRequest);
0299     d->state = Private::P_USERNAME;
0300     Q_EMIT signalBusy(true);
0301 }
0302 
0303 /**
0304  * Get list of boards by parsing json sent by pinterest
0305  */
0306 void PTalker::listBoards(const QString& /*path*/)
0307 {
0308     QUrl url(QLatin1String("https://api.pinterest.com/v5/boards"));
0309 
0310     QNetworkRequest netRequest(url);
0311     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0312 
0313     d->reply = d->netMngr->get(netRequest);
0314 
0315     d->state = Private::P_LISTBOARDS;
0316     Q_EMIT signalBusy(true);
0317 }
0318 
0319 bool PTalker::addPin(const QString& imgPath,
0320                      const QString& boardID,
0321                      bool rescale,
0322                      int maxDim,
0323                      int imageQuality)
0324 {
0325     if (d->reply)
0326     {
0327         d->reply->abort();
0328         d->reply = nullptr;
0329     }
0330 
0331     Q_EMIT signalBusy(true);
0332 
0333     QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
0334 
0335     if (image.isNull())
0336     {
0337         Q_EMIT signalBusy(false);
0338         return false;
0339     }
0340 
0341     QString path = WSToolUtils::makeTemporaryDir("pinterest").filePath(QFileInfo(imgPath)
0342                                                  .baseName().trimmed() + QLatin1String(".jpg"));
0343 
0344     if (rescale && ((image.width() > maxDim) || (image.height() > maxDim)))
0345     {
0346         image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0347     }
0348 
0349     image.save(path, "JPEG", imageQuality);
0350 
0351     QScopedPointer<DMetadata> meta(new DMetadata);
0352 
0353     if (meta->load(imgPath))
0354     {
0355         meta->setItemDimensions(image.size());
0356         meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
0357         meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
0358         meta->save(path, true);
0359     }
0360 
0361     QFile file(imgPath);
0362 
0363     if (!file.open(QIODevice::ReadOnly))
0364     {
0365         return false;
0366     }
0367 
0368     QByteArray fileData = file.readAll();
0369     file.close();
0370 
0371     QUrl url(QLatin1String("https://api.pinterest.com/v5/pins"));
0372 
0373     QNetworkRequest netRequest(url);
0374     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0375     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0376 
0377     QByteArray postData;
0378     postData += "{\"board_id\": \"";
0379     postData += boardID.toLatin1();
0380     postData += "\",\"media_source\": {";
0381     postData += "\"source_type\": \"image_base64\",";
0382     postData += "\"content_type\": \"image/jpeg\",";
0383     postData += "\"data\": \"";
0384     postData += fileData.toBase64();
0385     postData += "\"}}";
0386 
0387     d->reply = d->netMngr->post(netRequest, postData);
0388     d->state = Private::P_ADDPIN;
0389 
0390     return true;
0391 }
0392 
0393 void PTalker::slotFinished(QNetworkReply* reply)
0394 {
0395     if (reply != d->reply)
0396     {
0397         return;
0398     }
0399 
0400     d->reply = nullptr;
0401 
0402     if (reply->error() != QNetworkReply::NoError)
0403     {
0404         if (d->state != Private::P_CREATEBOARD)
0405         {
0406             Q_EMIT signalBusy(false);
0407             QMessageBox::critical(QApplication::activeWindow(),
0408                                   i18nc("@title:window", "Error"), reply->errorString());
0409 /*
0410             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error content: " << reply->readAll();
0411 */
0412             Q_EMIT signalNetworkError();
0413 
0414             reply->deleteLater();
0415             return;
0416         }
0417     }
0418 
0419     QByteArray buffer = reply->readAll();
0420 /*
0421     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "BUFFER" << buffer;
0422 */
0423     switch (d->state)
0424     {
0425         case Private::P_LISTBOARDS:
0426         {
0427             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_LISTBOARDS";
0428             parseResponseListBoards(buffer);
0429             break;
0430         }
0431 
0432         case Private::P_CREATEBOARD:
0433         {
0434             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_CREATEBOARD";
0435             parseResponseCreateBoard(buffer);
0436             break;
0437         }
0438 
0439         case Private::P_ADDPIN:
0440         {
0441             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_ADDPIN";
0442             parseResponseAddPin(buffer);
0443             break;
0444         }
0445 
0446         case Private::P_USERNAME:
0447         {
0448             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_USERNAME";
0449             parseResponseUserName(buffer);
0450             break;
0451         }
0452 
0453         case Private::P_ACCESSTOKEN:
0454         {
0455             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In P_ACCESSTOKEN";
0456             parseResponseAccessToken(buffer);
0457             break;
0458         }
0459 
0460         default:
0461             break;
0462     }
0463 
0464     reply->deleteLater();
0465 }
0466 
0467 void PTalker::parseResponseAccessToken(const QByteArray& data)
0468 {
0469     QJsonDocument doc      = QJsonDocument::fromJson(data);
0470     QJsonObject jsonObject = doc.object();
0471     d->accessToken         = jsonObject[QLatin1String("access_token")].toString();
0472 
0473     if (!d->accessToken.isEmpty())
0474     {
0475         qDebug(DIGIKAM_WEBSERVICES_LOG) << "Access token Received:" << d->accessToken;
0476         Q_EMIT pinterestLinkingSucceeded();
0477     }
0478     else
0479     {
0480         Q_EMIT pinterestLinkingFailed();
0481     }
0482 
0483     Q_EMIT signalBusy(false);
0484 }
0485 
0486 void PTalker::parseResponseAddPin(const QByteArray& data)
0487 {
0488     QJsonDocument doc      = QJsonDocument::fromJson(data);
0489     QJsonObject jsonObject = doc.object();
0490     bool success           = jsonObject.contains(QLatin1String("id"));
0491     Q_EMIT signalBusy(false);
0492 
0493     if (!success)
0494     {
0495         Q_EMIT signalAddPinFailed(i18n("Failed to upload Pin"));
0496     }
0497     else
0498     {
0499         Q_EMIT signalAddPinSucceeded();
0500     }
0501 }
0502 
0503 void PTalker::parseResponseUserName(const QByteArray& data)
0504 {
0505     QJsonDocument doc      = QJsonDocument::fromJson(data);
0506     QJsonObject jsonObject = doc.object();
0507     d->userName            = jsonObject[QLatin1String("username")].toString();
0508 
0509     Q_EMIT signalBusy(false);
0510     Q_EMIT signalSetUserName(d->userName);
0511 }
0512 
0513 void PTalker::parseResponseListBoards(const QByteArray& data)
0514 {
0515     QJsonParseError err;
0516     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0517 
0518     if (err.error != QJsonParseError::NoError)
0519     {
0520         Q_EMIT signalBusy(false);
0521         Q_EMIT signalListBoardsFailed(i18n("Failed to list boards"));
0522         return;
0523     }
0524 
0525     QJsonObject jsonObject = doc.object();
0526 /*
0527     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Json Listing Boards:" << doc;
0528 */
0529     QJsonArray jsonArray   = jsonObject[QLatin1String("items")].toArray();
0530 
0531     QList<QPair<QString, QString> > list;
0532 
0533     Q_FOREACH (const QJsonValue& value, jsonArray)
0534     {
0535         QString boardID;
0536         QString boardName;
0537         QJsonObject obj = value.toObject();
0538         boardID         = obj[QLatin1String("id")].toString();
0539         boardName       = obj[QLatin1String("name")].toString();
0540 
0541         list.append(qMakePair(boardID, boardName));
0542     }
0543 
0544     Q_EMIT signalBusy(false);
0545     Q_EMIT signalListBoardsDone(list);
0546 }
0547 
0548 void PTalker::parseResponseCreateBoard(const QByteArray& data)
0549 {
0550     QJsonDocument doc1     = QJsonDocument::fromJson(data);
0551     QJsonObject jsonObject = doc1.object();
0552     bool fail              = jsonObject.contains(QLatin1String("code"));
0553 
0554     Q_EMIT signalBusy(false);
0555 
0556     if (fail)
0557     {
0558         QJsonParseError err;
0559         QJsonDocument doc2 = QJsonDocument::fromJson(data, &err);
0560         Q_EMIT signalCreateBoardFailed(jsonObject[QLatin1String("message")].toString());
0561     }
0562     else
0563     {
0564         Q_EMIT signalCreateBoardSucceeded();
0565     }
0566 }
0567 
0568 void PTalker::writeSettings()
0569 {
0570     d->settings->beginGroup(d->serviceName);
0571     d->settings->setValue(d->serviceKey, d->accessToken);
0572     d->settings->endGroup();
0573 }
0574 
0575 void PTalker::readSettings()
0576 {
0577     d->settings->beginGroup(d->serviceName);
0578     d->accessToken = d->settings->value(d->serviceKey).toString();
0579     d->settings->endGroup();
0580 
0581     if (d->accessToken.isEmpty())
0582     {
0583         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Linking...";
0584         link();
0585     }
0586     else
0587     {
0588         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Already Linked";
0589         Q_EMIT pinterestLinkingSucceeded();
0590     }
0591 }
0592 
0593 } // namespace DigikamGenericPinterestPlugin
0594 
0595 #include "moc_ptalker.cpp"