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"