File indexing completed on 2024-05-05 04:57:14

0001 /*
0002     This file is part of Choqok, the KDE micro-blogging client
0003 
0004     SPDX-FileCopyrightText: 2008-2012 Mehrdad Momeny <mehrdad.momeny@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "gnusocialapimicroblog.h"
0010 
0011 #include <QJsonArray>
0012 #include <QJsonDocument>
0013 #include <QJsonObject>
0014 #include <QMimeDatabase>
0015 
0016 #include <KIO/StoredTransferJob>
0017 #include <KJobWidgets>
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 
0021 #include "account.h"
0022 #include "accountmanager.h"
0023 #include "choqokappearancesettings.h"
0024 #include "composerwidget.h"
0025 #include "editaccountwidget.h"
0026 #include "mediamanager.h"
0027 #include "microblogwidget.h"
0028 #include "postwidget.h"
0029 #include "timelinewidget.h"
0030 
0031 #include "twitterapimicroblogwidget.h"
0032 #include "twitterapipostwidget.h"
0033 #include "twitterapitimelinewidget.h"
0034 
0035 #include "gnusocialapiaccount.h"
0036 #include "gnusocialapicomposerwidget.h"
0037 #include "gnusocialapidebug.h"
0038 #include "gnusocialapidmessagedialog.h"
0039 #include "gnusocialapipostwidget.h"
0040 #include "gnusocialapisearch.h"
0041 
0042 GNUSocialApiMicroBlog::GNUSocialApiMicroBlog(const QString &componentName, QObject *parent = nullptr)
0043     : TwitterApiMicroBlog(componentName, parent), friendsPage(1)
0044 {
0045     qCDebug(CHOQOK);
0046     setServiceName(QLatin1String("GNU social"));
0047     mTimelineInfos[QLatin1String("ReTweets")]->name = i18nc("Timeline name", "Repeated");
0048     mTimelineInfos[QLatin1String("ReTweets")]->description = i18nc("Timeline description", "Your posts that were repeated by others");
0049 }
0050 
0051 GNUSocialApiMicroBlog::~GNUSocialApiMicroBlog()
0052 {
0053     qCDebug(CHOQOK);
0054 }
0055 
0056 Choqok::Account *GNUSocialApiMicroBlog::createNewAccount(const QString &alias)
0057 {
0058     GNUSocialApiAccount *acc = qobject_cast<GNUSocialApiAccount *>(Choqok::AccountManager::self()->findAccount(alias));
0059     if (!acc) {
0060         return new GNUSocialApiAccount(this, alias);
0061     } else {
0062         return nullptr;
0063     }
0064 }
0065 
0066 Choqok::UI::MicroBlogWidget *GNUSocialApiMicroBlog::createMicroBlogWidget(Choqok::Account *account, QWidget *parent)
0067 {
0068     return new TwitterApiMicroBlogWidget(account, parent);
0069 }
0070 
0071 Choqok::UI::TimelineWidget *GNUSocialApiMicroBlog::createTimelineWidget(Choqok::Account *account,
0072         const QString &timelineName, QWidget *parent)
0073 {
0074     return new TwitterApiTimelineWidget(account, timelineName, parent);
0075 }
0076 
0077 Choqok::UI::PostWidget *GNUSocialApiMicroBlog::createPostWidget(Choqok::Account *account,
0078         Choqok::Post *post, QWidget *parent)
0079 {
0080     return new GNUSocialApiPostWidget(account, post, parent);
0081 }
0082 
0083 Choqok::UI::ComposerWidget *GNUSocialApiMicroBlog::createComposerWidget(Choqok::Account *account, QWidget *parent)
0084 {
0085     return new GNUSocialApiComposerWidget(account, parent);
0086 }
0087 
0088 Choqok::Post *GNUSocialApiMicroBlog::readPost(Choqok::Account *account, const QVariantMap &var, Choqok::Post *post)
0089 {
0090     if (!post) {
0091         qCCritical(CHOQOK) << "post is NULL!";
0092         return nullptr;
0093     }
0094 
0095     if (var[QLatin1String("source")].toString().compare(QLatin1String("linkback")) == 0) {
0096         // Skip linkback statuses
0097         return nullptr;
0098     }
0099 
0100     post = TwitterApiMicroBlog::readPost(account, var, post);
0101 
0102     QUrl profileUrl = var[QLatin1String("user")].toMap()[QLatin1String("statusnet_profile_url")].toUrl();
0103     post->author.homePageUrl = profileUrl;
0104 
0105     const QVariantMap retweeted = var[QLatin1String("retweeted_status")].toMap();
0106 
0107     if (!retweeted.isEmpty()) {
0108         profileUrl = retweeted[QLatin1String("user")].toMap()[QLatin1String("statusnet_profile_url")].toUrl();
0109         post->repeatedFromUser.homePageUrl = profileUrl;
0110     }
0111 
0112     if (var.contains(QLatin1String("uri"))) {
0113         post->link = var[QLatin1String("uri")].toUrl();
0114     } else if (var.contains(QLatin1String("external_url"))) {
0115         post->link = var[QLatin1String("external_url")].toUrl();
0116     } else {
0117         if (retweeted.contains(QLatin1String("uri"))) {
0118             post->link = var[QLatin1String("uri")].toUrl();
0119         } else {
0120             // Last try, compone the url. However this only works for GNU Social instances.
0121             post->link = QUrl::fromUserInput(QStringLiteral("%1://%2/notice/%3")
0122                                              .arg(profileUrl.scheme()).arg(profileUrl.host()).arg(post->postId));
0123         }
0124     }
0125 
0126     return post;
0127 }
0128 
0129 QUrl GNUSocialApiMicroBlog::profileUrl(Choqok::Account *account, const QString &username) const
0130 {
0131     if (username.contains(QLatin1Char('@'))) {
0132         const QStringList lst = username.split(QLatin1Char('@'), QString::SkipEmptyParts);
0133 
0134         if (lst.count() == 2) {
0135             return QUrl::fromUserInput(QStringLiteral("https://%1/%2").arg(lst[1]).arg(lst[0]));
0136         } else {
0137             return QUrl();
0138         }
0139     } else {
0140         GNUSocialApiAccount *acc = qobject_cast<GNUSocialApiAccount *>(account);
0141 
0142         QUrl url(acc->host());
0143         url = url.adjusted(QUrl::StripTrailingSlash);
0144         url.setPath(QLatin1Char('/') + username);
0145 
0146         return url;
0147     }
0148 }
0149 
0150 QUrl GNUSocialApiMicroBlog::postUrl(Choqok::Account *account,  const QString &username,
0151                                    const QString &postId) const
0152 {
0153     Q_UNUSED(username)
0154     TwitterApiAccount *acc = qobject_cast<TwitterApiAccount *>(account);
0155     if (acc) {
0156         QUrl url(acc->homepageUrl());
0157         url.setPath(url.path() + QStringLiteral("/notice/%1").arg(postId));
0158         return url;
0159     } else {
0160         return QUrl();
0161     }
0162 }
0163 
0164 TwitterApiSearch *GNUSocialApiMicroBlog::searchBackend()
0165 {
0166     if (!mSearchBackend) {
0167         mSearchBackend = new GNUSocialApiSearch(this);
0168     }
0169     return mSearchBackend;
0170 }
0171 
0172 void GNUSocialApiMicroBlog::createPostWithAttachment(Choqok::Account *theAccount, Choqok::Post *post,
0173         const QString &mediumToAttach)
0174 {
0175     if (mediumToAttach.isEmpty()) {
0176         TwitterApiMicroBlog::createPost(theAccount, post);
0177     } else {
0178         const QUrl picUrl = QUrl::fromUserInput(mediumToAttach);
0179         KIO::StoredTransferJob *picJob = KIO::storedGet(picUrl, KIO::Reload, KIO::HideProgressInfo);
0180         picJob->exec();
0181         if (picJob->error()) {
0182             qCCritical(CHOQOK) << "Job error:" << picJob->errorString();
0183             KMessageBox::detailedError(Choqok::UI::Global::mainWindow(),
0184                                        i18n("Uploading medium failed: cannot read the medium file."),
0185                                        picJob->errorString());
0186             return;
0187         }
0188         const QByteArray picData = picJob->data();
0189         if (picData.count() == 0) {
0190             qCCritical(CHOQOK) << "Cannot read the media file, please check if it exists.";
0191             KMessageBox::error(Choqok::UI::Global::mainWindow(),
0192                                i18n("Uploading medium failed: cannot read the medium file."));
0193             return;
0194         }
0195         ///Documentation: http://identi.ca/notice/17779990
0196         TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
0197         QUrl url = account->apiUrl();
0198         url.setPath(url.path() + QLatin1String("/statuses/update.json"));
0199         const QMimeDatabase db;
0200         QByteArray fileContentType = db.mimeTypeForUrl(picUrl).name().toUtf8();
0201 
0202         QMap<QString, QByteArray> formdata;
0203         formdata[QLatin1String("status")] = post->content.toUtf8();
0204         formdata[QLatin1String("in_reply_to_status_id")] = post->replyToPostId.toLatin1();
0205         formdata[QLatin1String("source")] = QCoreApplication::applicationName().toLatin1();
0206 
0207         QMap<QString, QByteArray> mediafile;
0208         mediafile[QLatin1String("name")] = "media";
0209         mediafile[QLatin1String("filename")] = picUrl.fileName().toUtf8();
0210         mediafile[QLatin1String("mediumType")] = fileContentType;
0211         mediafile[QLatin1String("medium")] = picData;
0212         QList< QMap<QString, QByteArray> > listMediafiles;
0213         listMediafiles.append(mediafile);
0214 
0215         QByteArray data = Choqok::MediaManager::createMultipartFormData(formdata, listMediafiles);
0216 
0217         KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo) ;
0218         if (!job) {
0219             qCCritical(CHOQOK) << "Cannot create a http POST request!";
0220             return;
0221         }
0222         job->addMetaData(QStringLiteral("content-type"),
0223                          QStringLiteral("Content-Type: multipart/form-data; boundary=AaB03x"));
0224         job->addMetaData(QStringLiteral("customHTTPHeader"),
0225                          QStringLiteral("Authorization: ") +
0226                          QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::PostOperation)));
0227         mCreatePostMap[ job ] = post;
0228         mJobsAccount[job] = theAccount;
0229         connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotCreatePost);
0230         job->start();
0231     }
0232 }
0233 
0234 QString GNUSocialApiMicroBlog::generateRepeatedByUserTooltip(const QString &username)
0235 {
0236     if (Choqok::AppearanceSettings::showRetweetsInChoqokWay()) {
0237         return i18n("Repeat of %1", username);
0238     } else {
0239         return i18n("Repeated by %1", username);
0240     }
0241 }
0242 
0243 QString GNUSocialApiMicroBlog::repeatQuestion()
0244 {
0245     return i18n("Repeat this notice?");
0246 }
0247 
0248 void GNUSocialApiMicroBlog::listFriendsUsername(TwitterApiAccount *theAccount, bool active)
0249 {
0250     Q_UNUSED(active);
0251     friendsList.clear();
0252     if (theAccount) {
0253         doRequestFriendsScreenName(theAccount, 1);
0254     }
0255 }
0256 
0257 QStringList GNUSocialApiMicroBlog::readFriendsScreenName(Choqok::Account *theAccount, const QByteArray &buffer)
0258 {
0259     QStringList list;
0260     const QJsonDocument json = QJsonDocument::fromJson(buffer);
0261     if (!json.isNull()) {
0262         for (const QJsonValue &u: json.array()) {
0263             const QJsonObject user = u.toObject();
0264 
0265             if (user.contains(QStringLiteral("statusnet_profile_url"))) {
0266                 list.append(user.value(QLatin1String("statusnet_profile_url")).toString());
0267             }
0268         }
0269     } else {
0270         QString err = i18n("Retrieving the friends list failed. The data returned from the server is corrupted.");
0271         qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer;
0272         Q_EMIT error(theAccount, ParsingError, err, Critical);
0273     }
0274     return list;
0275 }
0276 
0277 void GNUSocialApiMicroBlog::requestFriendsScreenName(TwitterApiAccount *theAccount, bool active)
0278 {
0279     Q_UNUSED(active);
0280     doRequestFriendsScreenName(theAccount, 1);
0281 }
0282 
0283 void GNUSocialApiMicroBlog::showDirectMessageDialog(TwitterApiAccount *theAccount, const QString &toUsername)
0284 {
0285     qCDebug(CHOQOK);
0286     if (!theAccount) {
0287         QAction *act = qobject_cast<QAction *>(sender());
0288         theAccount = qobject_cast<TwitterApiAccount *>(Choqok::AccountManager::self()->findAccount(act->data().toString()));
0289     }
0290     GNUSocialApiDMessageDialog *dmsg = new GNUSocialApiDMessageDialog(theAccount, Choqok::UI::Global::mainWindow());
0291     if (!toUsername.isEmpty()) {
0292         dmsg->setTo(toUsername);
0293     }
0294     dmsg->show();
0295 }
0296 
0297 void GNUSocialApiMicroBlog::doRequestFriendsScreenName(TwitterApiAccount *theAccount, int page)
0298 {
0299     qCDebug(CHOQOK);
0300     TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
0301     QUrl url = account->apiUrl();
0302     url = url.adjusted(QUrl::StripTrailingSlash);
0303     url.setPath(url.path() + QLatin1String("/statuses/friends.json"));
0304 
0305     if (page > 1) {
0306         QUrlQuery urlQuery;
0307         urlQuery.addQueryItem(QLatin1String("page"), QString::number(page));
0308         url.setQuery(urlQuery);
0309     }
0310 
0311     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo) ;
0312     if (!job) {
0313         qCDebug(CHOQOK) << "Cannot create an http GET request!";
0314         return;
0315     }
0316     job->addMetaData(QStringLiteral("customHTTPHeader"),
0317                      QStringLiteral("Authorization: ") +
0318                      QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::GetOperation)));
0319     mJobsAccount[job] = theAccount;
0320     connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotRequestFriendsScreenName);
0321     job->start();
0322 }
0323 
0324 void GNUSocialApiMicroBlog::slotRequestFriendsScreenName(KJob *job)
0325 {
0326     qCDebug(CHOQOK);
0327     TwitterApiAccount *theAccount = qobject_cast<TwitterApiAccount *>(mJobsAccount.take(job));
0328     if (job->error()) {
0329         Q_EMIT error(theAccount, ServerError, i18n("Friends list for account %1 could not be updated:\n%2",
0330                      theAccount->username(), job->errorString()), Normal);
0331         return;
0332     }
0333     KIO::StoredTransferJob *stJob = qobject_cast<KIO::StoredTransferJob *>(job);
0334     QStringList newList = readFriendsScreenName(theAccount, stJob->data());
0335     friendsList << newList;
0336     if (newList.count() == 100) {
0337         doRequestFriendsScreenName(theAccount, ++friendsPage);
0338     } else {
0339         friendsList.removeDuplicates();
0340         theAccount->setFriendsList(friendsList);
0341         Q_EMIT friendsUsernameListed(theAccount, friendsList);
0342     }
0343 }
0344 
0345 void GNUSocialApiMicroBlog::fetchConversation(Choqok::Account *theAccount, const QString &conversationId)
0346 {
0347     qCDebug(CHOQOK);
0348     if (conversationId.isEmpty()) {
0349         return;
0350     }
0351     TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
0352     QUrl url = account->apiUrl();
0353     url.setPath(QStringLiteral("/statusnet/conversation/%1.json").arg(conversationId));
0354 
0355     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo) ;
0356     if (!job) {
0357         qCDebug(CHOQOK) << "Cannot create an http GET request!";
0358         return;
0359     }
0360     job->addMetaData(QStringLiteral("customHTTPHeader"),
0361                      QStringLiteral("Authorization: ") +
0362                      QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::GetOperation)));
0363     mFetchConversationMap[ job ] = conversationId;
0364     mJobsAccount[ job ] = theAccount;
0365     connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotFetchConversation);
0366     job->start();
0367 }
0368 
0369 QString GNUSocialApiMicroBlog::usernameFromProfileUrl(const QString &profileUrl)
0370 {
0371     // Remove the initial slash from path
0372     return QUrl(profileUrl).path().remove(0, 1);
0373 }
0374 
0375 QString GNUSocialApiMicroBlog::hostFromProfileUrl(const QString &profileUrl)
0376 {
0377     return QUrl(profileUrl).host();
0378 }
0379 
0380 void GNUSocialApiMicroBlog::slotFetchConversation(KJob *job)
0381 {
0382     qCDebug(CHOQOK);
0383     if (!job) {
0384         qCWarning(CHOQOK) << "NULL Job returned";
0385         return;
0386     }
0387     QList<Choqok::Post *> posts;
0388     QString conversationId = mFetchConversationMap.take(job);
0389     Choqok::Account *theAccount = mJobsAccount.take(job);
0390     if (job->error()) {
0391         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0392         Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError,
0393                      i18n("Fetching conversation failed. %1", job->errorString()), Normal);
0394     } else {
0395         KIO::StoredTransferJob *stj = qobject_cast<KIO::StoredTransferJob *> (job);
0396         //if(format=="json"){
0397         posts = readTimeline(theAccount, stj->data());
0398         //} else {
0399         //    posts = readTimelineFromXml ( theAccount, stj->data() );
0400         //}
0401         if (!posts.isEmpty()) {
0402             Q_EMIT conversationFetched(theAccount, conversationId, posts);
0403         }
0404     }
0405 }
0406 
0407 #include "moc_gnusocialapimicroblog.cpp"