328 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			328 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // For license of this file, see <project-root-folder>/LICENSE.md.
 | |
| 
 | |
| #include "services/reddit/redditnetworkfactory.h"
 | |
| 
 | |
| #include "database/databasequeries.h"
 | |
| #include "definitions/definitions.h"
 | |
| #include "exceptions/applicationexception.h"
 | |
| #include "exceptions/networkexception.h"
 | |
| #include "gui/dialogs/formmain.h"
 | |
| #include "gui/tabwidget.h"
 | |
| #include "miscellaneous/application.h"
 | |
| #include "miscellaneous/textfactory.h"
 | |
| #include "network-web/networkfactory.h"
 | |
| #include "network-web/oauth2service.h"
 | |
| #include "network-web/silentnetworkaccessmanager.h"
 | |
| #include "network-web/webfactory.h"
 | |
| #include "services/abstract/category.h"
 | |
| #include "services/reddit/definitions.h"
 | |
| #include "services/reddit/redditserviceroot.h"
 | |
| #include "services/reddit/redditsubscription.h"
 | |
| 
 | |
| #include <QHttpMultiPart>
 | |
| #include <QJsonArray>
 | |
| #include <QJsonDocument>
 | |
| #include <QJsonObject>
 | |
| #include <QRegularExpression>
 | |
| #include <QThread>
 | |
| #include <QUrl>
 | |
| 
 | |
| RedditNetworkFactory::RedditNetworkFactory(QObject* parent)
 | |
|   : QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(REDDIT_DEFAULT_BATCH_SIZE),
 | |
|     m_downloadOnlyUnreadMessages(false), m_oauth2(new OAuth2Service(QSL(REDDIT_OAUTH_AUTH_URL),
 | |
|                                                                     QSL(REDDIT_OAUTH_TOKEN_URL),
 | |
|                                                                     {},
 | |
|                                                                     {},
 | |
|                                                                     QSL(REDDIT_OAUTH_SCOPE),
 | |
|                                                                     this)) {
 | |
|   initializeOauth();
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::setService(RedditServiceRoot* service) {
 | |
|   m_service = service;
 | |
| }
 | |
| 
 | |
| OAuth2Service* RedditNetworkFactory::oauth() const {
 | |
|   return m_oauth2;
 | |
| }
 | |
| 
 | |
| QString RedditNetworkFactory::username() const {
 | |
|   return m_username;
 | |
| }
 | |
| 
 | |
| int RedditNetworkFactory::batchSize() const {
 | |
|   return m_batchSize;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::setBatchSize(int batch_size) {
 | |
|   m_batchSize = batch_size;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::initializeOauth() {
 | |
|   m_oauth2->setUseHttpBasicAuthWithClientData(true);
 | |
|   m_oauth2->setRedirectUrl(QSL(OAUTH_REDIRECT_URI) + QL1C(':') + QString::number(REDDIT_OAUTH_REDIRECT_URI_PORT), true);
 | |
| 
 | |
|   connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &RedditNetworkFactory::onTokensError);
 | |
|   connect(m_oauth2, &OAuth2Service::authFailed, this, &RedditNetworkFactory::onAuthFailed);
 | |
|   connect(m_oauth2,
 | |
|           &OAuth2Service::tokensRetrieved,
 | |
|           this,
 | |
|           [this](QString access_token, QString refresh_token, int expires_in) {
 | |
|             Q_UNUSED(expires_in)
 | |
|             Q_UNUSED(access_token)
 | |
| 
 | |
|             if (m_service != nullptr && !refresh_token.isEmpty()) {
 | |
|               QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
 | |
| 
 | |
|               DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId());
 | |
|             }
 | |
|           });
 | |
| }
 | |
| 
 | |
| bool RedditNetworkFactory::downloadOnlyUnreadMessages() const {
 | |
|   return m_downloadOnlyUnreadMessages;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread_messages) {
 | |
|   m_downloadOnlyUnreadMessages = download_only_unread_messages;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::setOauth(OAuth2Service* oauth) {
 | |
|   m_oauth2 = oauth;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::setUsername(const QString& username) {
 | |
|   m_username = username;
 | |
| }
 | |
| 
 | |
| QVariantHash RedditNetworkFactory::me(const QNetworkProxy& custom_proxy) {
 | |
|   QString bearer = m_oauth2->bearer().toLocal8Bit();
 | |
| 
 | |
|   if (bearer.isEmpty()) {
 | |
|     throw ApplicationException(tr("you are not logged in"));
 | |
|   }
 | |
| 
 | |
|   QList<QPair<QByteArray, QByteArray>> headers;
 | |
| 
 | |
|   headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
 | |
|                                                m_oauth2->bearer().toLocal8Bit()));
 | |
| 
 | |
|   int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
 | |
|   QByteArray output;
 | |
|   auto result = NetworkFactory::performNetworkOperation(QSL(REDDIT_API_GET_PROFILE),
 | |
|                                                         timeout,
 | |
|                                                         {},
 | |
|                                                         output,
 | |
|                                                         QNetworkAccessManager::Operation::GetOperation,
 | |
|                                                         headers,
 | |
|                                                         false,
 | |
|                                                         {},
 | |
|                                                         {},
 | |
|                                                         custom_proxy)
 | |
|                   .m_networkError;
 | |
| 
 | |
|   if (result != QNetworkReply::NetworkError::NoError) {
 | |
|     throw NetworkException(result, output);
 | |
|   }
 | |
|   else {
 | |
|     QJsonDocument doc = QJsonDocument::fromJson(output);
 | |
| 
 | |
|     return doc.object().toVariantHash();
 | |
|   }
 | |
| }
 | |
| 
 | |
| QList<Feed*> RedditNetworkFactory::subreddits(const QNetworkProxy& custom_proxy) {
 | |
|   QString bearer = m_oauth2->bearer().toLocal8Bit();
 | |
| 
 | |
|   if (bearer.isEmpty()) {
 | |
|     throw ApplicationException(tr("you are not logged in"));
 | |
|   }
 | |
| 
 | |
|   QList<QPair<QByteArray, QByteArray>> headers;
 | |
| 
 | |
|   headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
 | |
|                                                m_oauth2->bearer().toLocal8Bit()));
 | |
| 
 | |
|   int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
 | |
|   QString after;
 | |
|   QList<Feed*> subs;
 | |
| 
 | |
|   do {
 | |
|     QByteArray output;
 | |
|     QString final_url = QSL(REDDIT_API_SUBREDDITS).arg(QString::number(100));
 | |
| 
 | |
|     if (!after.isEmpty()) {
 | |
|       final_url += QSL("&after=%1").arg(after);
 | |
|     }
 | |
| 
 | |
|     auto result = NetworkFactory::performNetworkOperation(final_url,
 | |
|                                                           timeout,
 | |
|                                                           {},
 | |
|                                                           output,
 | |
|                                                           QNetworkAccessManager::Operation::GetOperation,
 | |
|                                                           headers,
 | |
|                                                           false,
 | |
|                                                           {},
 | |
|                                                           {},
 | |
|                                                           custom_proxy)
 | |
|                     .m_networkError;
 | |
| 
 | |
|     if (result != QNetworkReply::NetworkError::NoError) {
 | |
|       throw NetworkException(result, output);
 | |
|     }
 | |
|     else {
 | |
|       QJsonDocument doc = QJsonDocument::fromJson(output);
 | |
|       QJsonObject root_doc = doc.object();
 | |
| 
 | |
|       after = root_doc["data"].toObject()["after"].toString();
 | |
| 
 | |
|       for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) {
 | |
|         const auto sub_obj = sub_val.toObject()["data"].toObject();
 | |
| 
 | |
|         RedditSubscription* new_sub = new RedditSubscription();
 | |
| 
 | |
|         new_sub->setCustomId(sub_obj["id"].toString());
 | |
|         new_sub->setTitle(sub_obj["title"].toString());
 | |
|         new_sub->setDescription(sub_obj["public_description"].toString());
 | |
| 
 | |
|         new_sub->setPrefixedName(sub_obj["url"].toString());
 | |
| 
 | |
|         QIcon icon;
 | |
|         QString icon_url = sub_obj["community_icon"].toString();
 | |
| 
 | |
|         if (icon_url.isEmpty()) {
 | |
|           icon_url = sub_obj["icon_img"].toString();
 | |
|         }
 | |
| 
 | |
|         if (icon_url.contains(QL1S("?"))) {
 | |
|           icon_url = icon_url.mid(0, icon_url.indexOf(QL1S("?")));
 | |
|         }
 | |
| 
 | |
|         if (!icon_url.isEmpty() &&
 | |
|             NetworkFactory::downloadIcon({{icon_url, true}}, timeout, icon, headers, custom_proxy) ==
 | |
|               QNetworkReply::NetworkError::NoError) {
 | |
|           new_sub->setIcon(icon);
 | |
|         }
 | |
| 
 | |
|         subs.append(new_sub);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   while (!after.isEmpty());
 | |
| 
 | |
|   // posty dle jmena redditu
 | |
|   // https://oauth.reddit.com/<SUBREDDIT>/new
 | |
|   //
 | |
|   // komenty pro post dle id postu
 | |
|   // https://oauth.reddit.com/<SUBREDDIT>/comments/<ID-POSTU>
 | |
| 
 | |
|   return subs;
 | |
| }
 | |
| 
 | |
| QList<Message> RedditNetworkFactory::hot(const QString& sub_name, const QNetworkProxy& custom_proxy) {
 | |
|   QString bearer = m_oauth2->bearer().toLocal8Bit();
 | |
| 
 | |
|   if (bearer.isEmpty()) {
 | |
|     throw ApplicationException(tr("you are not logged in"));
 | |
|   }
 | |
| 
 | |
|   QList<QPair<QByteArray, QByteArray>> headers;
 | |
| 
 | |
|   headers.append(QPair<QByteArray, QByteArray>(QSL(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
 | |
|                                                m_oauth2->bearer().toLocal8Bit()));
 | |
| 
 | |
|   int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
 | |
|   QString after;
 | |
|   QList<Message> msgs;
 | |
| 
 | |
|   int desired_count = batchSize();
 | |
| 
 | |
|   do {
 | |
|     int next_batch = desired_count <= 0 ? 100 : std::min(100, int(desired_count - msgs.size()));
 | |
| 
 | |
|     QByteArray output;
 | |
|     QString final_url =
 | |
|       QSL(REDDIT_API_HOT).arg(sub_name, QString::number(next_batch), QString::number(msgs.size()), QSL("GLOBAL"));
 | |
| 
 | |
|     if (!after.isEmpty()) {
 | |
|       final_url += QSL("&after=%1").arg(after);
 | |
|     }
 | |
| 
 | |
|     auto result = NetworkFactory::performNetworkOperation(final_url,
 | |
|                                                           timeout,
 | |
|                                                           {},
 | |
|                                                           output,
 | |
|                                                           QNetworkAccessManager::Operation::GetOperation,
 | |
|                                                           headers,
 | |
|                                                           false,
 | |
|                                                           {},
 | |
|                                                           {},
 | |
|                                                           custom_proxy)
 | |
|                     .m_networkError;
 | |
| 
 | |
|     if (result != QNetworkReply::NetworkError::NoError) {
 | |
|       throw NetworkException(result, output);
 | |
|     }
 | |
|     else {
 | |
|       QJsonDocument doc = QJsonDocument::fromJson(output);
 | |
|       QJsonObject root_doc = doc.object();
 | |
| 
 | |
|       after = root_doc["data"].toObject()["after"].toString();
 | |
| 
 | |
|       for (const QJsonValue& sub_val : root_doc["data"].toObject()["children"].toArray()) {
 | |
|         const auto msg_obj = sub_val.toObject()["data"].toObject();
 | |
| 
 | |
|         Message new_msg;
 | |
| 
 | |
|         new_msg.m_customId = msg_obj["id"].toString();
 | |
|         new_msg.m_title = msg_obj["title"].toString();
 | |
|         new_msg.m_author = msg_obj["author"].toString();
 | |
|         new_msg.m_createdFromFeed = true;
 | |
|         new_msg.m_created =
 | |
|           QDateTime::fromSecsSinceEpoch(msg_obj["created_utc"].toVariant().toLongLong(), Qt::TimeSpec::UTC);
 | |
|         new_msg.m_url = QSL("https://reddit.com") + msg_obj["permalink"].toString();
 | |
|         new_msg.m_contents =
 | |
|           msg_obj["description_html"]
 | |
|             .toString(); // když prazdny, je poustnutej třeba obrazek či odkaz?, viz property "post_hint"?
 | |
|         new_msg.m_rawContents = QJsonDocument(msg_obj).toJson(QJsonDocument::JsonFormat::Compact);
 | |
| 
 | |
|         msgs.append(new_msg);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   while (!after.isEmpty() && (desired_count <= 0 || desired_count > msgs.size()));
 | |
| 
 | |
|   // posty dle jmena redditu
 | |
|   // https://oauth.reddit.com/<SUBREDDIT>/new
 | |
|   //
 | |
|   // komenty pro post dle id postu
 | |
|   // https://oauth.reddit.com/<SUBREDDIT>/comments/<ID-POSTU>
 | |
| 
 | |
|   return msgs;
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::onTokensError(const QString& error, const QString& error_description) {
 | |
|   Q_UNUSED(error)
 | |
| 
 | |
|   qApp->showGuiMessage(Notification::Event::LoginFailure,
 | |
|                        {tr("Reddit: authentication error"),
 | |
|                         tr("Click this to login again. Error is: '%1'").arg(error_description),
 | |
|                         QSystemTrayIcon::MessageIcon::Critical},
 | |
|                        {},
 | |
|                        {tr("Login"), [this]() {
 | |
|                           m_oauth2->setAccessToken(QString());
 | |
|                           m_oauth2->setRefreshToken(QString());
 | |
|                           m_oauth2->login();
 | |
|                         }});
 | |
| }
 | |
| 
 | |
| void RedditNetworkFactory::onAuthFailed() {
 | |
|   qApp->showGuiMessage(Notification::Event::LoginFailure,
 | |
|                        {tr("Reddit: authorization denied"),
 | |
|                         tr("Click this to login again."),
 | |
|                         QSystemTrayIcon::MessageIcon::Critical},
 | |
|                        {},
 | |
|                        {tr("Login"), [this]() {
 | |
|                           m_oauth2->login();
 | |
|                         }});
 | |
| }
 |