File indexing completed on 2024-04-28 04:55:45

0001 /*
0002     This file is part of Choqok, the KDE micro-blogging client
0003 
0004     SPDX-FileCopyrightText: 2017 Andrea Scarpino <scarpino@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "mastodonmicroblog.h"
0010 
0011 #include <QAction>
0012 #include <QFile>
0013 #include <QJsonArray>
0014 #include <QJsonDocument>
0015 #include <QMenu>
0016 #include <QMimeDatabase>
0017 #include <QTextDocument>
0018 
0019 #include <KIO/StoredTransferJob>
0020 #include <KPluginFactory>
0021 
0022 #include "accountmanager.h"
0023 #include "application.h"
0024 #include "choqokappearancesettings.h"
0025 #include "choqokbehaviorsettings.h"
0026 #include "notifymanager.h"
0027 #include "postwidget.h"
0028 
0029 #include "mastodonaccount.h"
0030 #include "mastodoncomposerwidget.h"
0031 #include "mastodondebug.h"
0032 #include "mastodondmessagedialog.h"
0033 #include "mastodoneditaccountwidget.h"
0034 #include "mastodonpost.h"
0035 #include "mastodonpostwidget.h"
0036 
0037 class MastodonMicroBlog::Private
0038 {
0039 public:
0040     Private(): countOfTimelinesToSave(0)
0041     {}
0042     int countOfTimelinesToSave;
0043 };
0044 
0045 K_PLUGIN_CLASS_WITH_JSON(MastodonMicroBlog, "choqok_mastodon.json")
0046 
0047 const QString MastodonMicroBlog::homeTimeline(QLatin1String("/api/v1/timelines/home"));
0048 const QString MastodonMicroBlog::publicTimeline(QLatin1String("/api/v1/timelines/public"));
0049 const QString MastodonMicroBlog::favouritesTimeline(QLatin1String("/api/v1/favourites"));
0050 
0051 MastodonMicroBlog::MastodonMicroBlog(QObject *parent, const QVariantList &args):
0052     MicroBlog(QStringLiteral("Mastodon") , parent), d(new Private)
0053 {
0054     Q_UNUSED(args)
0055     setServiceName(QLatin1String("Mastodon"));
0056     setServiceHomepageUrl(QLatin1String("https://mastodon.social"));
0057     QStringList timelineNames;
0058     timelineNames << QLatin1String("Home") << QLatin1String("Local") << QLatin1String("Federated") << QLatin1String("Favourites");
0059     setTimelineNames(timelineNames);
0060     setTimelinesInfo();
0061 }
0062 
0063 MastodonMicroBlog::~MastodonMicroBlog()
0064 {
0065     qDeleteAll(m_timelinesInfos);
0066     delete d;
0067 }
0068 
0069 void MastodonMicroBlog::aboutToUnload()
0070 {
0071     for (Choqok::Account *acc: Choqok::AccountManager::self()->accounts()) {
0072         if (acc->microblog() == this) {
0073             d->countOfTimelinesToSave += acc->timelineNames().count();
0074         }
0075     }
0076     Q_EMIT saveTimelines();
0077 }
0078 
0079 ChoqokEditAccountWidget *MastodonMicroBlog::createEditAccountWidget(Choqok::Account *account,
0080         QWidget *parent)
0081 {
0082     MastodonAccount *acc = qobject_cast<MastodonAccount * >(account);
0083     if (acc || !account) {
0084         return new MastodonEditAccountWidget(this, acc, parent);
0085     } else {
0086         qCDebug(CHOQOK) << "Account passed here was not a valid MastodonAccount!";
0087         return nullptr;
0088     }
0089 }
0090 
0091 Choqok::UI::ComposerWidget *MastodonMicroBlog::createComposerWidget(Choqok::Account *account, QWidget *parent)
0092 {
0093     return new MastodonComposerWidget(account, parent);
0094 }
0095 
0096 void MastodonMicroBlog::createPost(Choqok::Account *theAccount, Choqok::Post *post)
0097 {
0098     if (!post || post->content.isEmpty()) {
0099         qCDebug(CHOQOK) << "ERROR: Status text is empty!";
0100         Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::OtherError,
0101                          i18n("Creating the new post failed. Text is empty."), MicroBlog::Critical);
0102         return;
0103     }
0104 
0105     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0106     if (acc) {
0107         QVariantMap object;
0108         object.insert(QLatin1String("status"), post->content);
0109 
0110         const QByteArray data = QJsonDocument::fromVariant(object).toJson();
0111 
0112         QUrl url(acc->host());
0113         url = url.adjusted(QUrl::StripTrailingSlash);
0114         url.setPath(url.path() + QLatin1String("/api/v1/statuses"));
0115         KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo);
0116         job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json"));
0117         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0118         if (!job) {
0119             qCDebug(CHOQOK) << "Cannot create an http POST request!";
0120             return;
0121         }
0122         m_accountJobs[job] = acc;
0123         m_createPostJobs[job] = post;
0124         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost);
0125         job->start();
0126     } else {
0127         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0128     }
0129 }
0130 
0131 Choqok::Account *MastodonMicroBlog::createNewAccount(const QString &alias)
0132 {
0133     MastodonAccount *acc = qobject_cast<MastodonAccount *>(
0134                              Choqok::AccountManager::self()->findAccount(alias));
0135     if (!acc) {
0136         return new MastodonAccount(this, alias);
0137     } else {
0138         qCDebug(CHOQOK) << "Cannot create a new MastodonAccount!";
0139         return nullptr;
0140     }
0141 }
0142 
0143 QString MastodonMicroBlog::lastTimelineId(Choqok::Account *theAccount,
0144                                           const QString &timeline) const
0145 {
0146     qCDebug(CHOQOK) << "Latest ID for timeline " << timeline << m_timelinesLatestIds[theAccount][timeline];
0147     return m_timelinesLatestIds[theAccount][timeline];
0148 }
0149 
0150 QList< Choqok::Post * > MastodonMicroBlog::readTimeline(const QByteArray &buffer)
0151 {
0152     QList<Choqok::Post * > posts;
0153     const QJsonDocument json = QJsonDocument::fromJson(buffer);
0154     if (!json.isNull()) {
0155         const QVariantList list = json.array().toVariantList();
0156         for (const QVariant &element: list) {
0157             posts.prepend(readPost(element.toMap(), new MastodonPost));
0158         }
0159     } else {
0160         qCDebug(CHOQOK) << "Cannot parse JSON reply";
0161     }
0162 
0163     return posts;
0164 }
0165 
0166 Choqok::Post *MastodonMicroBlog::readPost(const QVariantMap &var, Choqok::Post *post)
0167 {
0168     MastodonPost *p = dynamic_cast< MastodonPost * >(post);
0169     if (p) {
0170         QVariantMap reblog = var[QLatin1String("reblog")].toMap();
0171         QVariantMap status;
0172         if (reblog.isEmpty()) {
0173             status = var;
0174         } else {
0175             status = reblog;
0176         }
0177 
0178         QTextDocument content;
0179         content.setHtml(status[QLatin1String("spoiler_text")].toString() + QLatin1String("<br />") + status[QLatin1String("content")].toString());
0180         p->content += content.toPlainText().trimmed();
0181 
0182         p->creationDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(),
0183                               Qt::ISODate);
0184         p->creationDateTime.setTimeSpec(Qt::UTC);
0185 
0186         p->link = status[QLatin1String("url")].toUrl();
0187         p->isFavorited = var[QLatin1String("favourited")].toBool();
0188         if (p->isFavorited) {
0189             p->isRead = true;
0190         }
0191         p->postId = var[QLatin1String("id")].toString();
0192 
0193         p->conversationId = var[QLatin1String("id")].toString();
0194 
0195         QVariantMap application = var[QLatin1String("application")].toMap();
0196         if (!application.isEmpty()) {
0197             const QString client = application[QLatin1String("name")].toString();
0198             if (application[QLatin1String("website")].toString().isEmpty()) {
0199                 p->source = client;
0200             } else {
0201                 p->source = QStringLiteral("<a href=\"%1\" rel=\"nofollow\">%2</a>").arg(application[QLatin1String("website")].toString()).arg(client);
0202             }
0203         }
0204 
0205         if (var[QLatin1String("visibility")].toString().compare(QLatin1String("direct")) == 0) {
0206             p->isPrivate = true;
0207         }
0208 
0209         QVariantMap account = status[QLatin1Literal("account")].toMap();
0210 
0211         p->author.userId = account[QLatin1String("id")].toString();
0212         p->author.userName = account[QLatin1String("acct")].toString();
0213         p->author.realName = account[QLatin1String("display_name")].toString();
0214         p->author.homePageUrl = account[QLatin1String("url")].toUrl();
0215 
0216         QTextDocument description;
0217         description.setHtml(account[QLatin1String("note")].toString());
0218         p->author.description = description.toPlainText().trimmed();
0219 
0220         p->author.profileImageUrl = account[QLatin1String("avatar")].toUrl();
0221 
0222         p->replyToPostId = var[QLatin1String("in_reply_to_id")].toString();
0223         p->replyToUser.userId = var[QLatin1String("in_reply_to_account_id")].toString();
0224 
0225         if (!reblog.isEmpty()) {
0226             p->repeatedDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(),
0227                               Qt::ISODate);
0228             p->repeatedDateTime.setTimeSpec(Qt::UTC);
0229 
0230             p->repeatedPostId = var[QLatin1String("id")].toString();
0231             const QVariantMap repeatedFrom = var[QLatin1Literal("account")].toMap();
0232             p->repeatedFromUser.userId = repeatedFrom[QLatin1String("id")].toString();
0233             p->repeatedFromUser.userName = repeatedFrom[QLatin1String("acct")].toString();
0234             p->repeatedFromUser.homePageUrl = repeatedFrom[QLatin1String("url")].toUrl();
0235         }
0236 
0237         return p;
0238     } else {
0239         qCDebug(CHOQOK) << "post is not a MastodonPost!";
0240         return post;
0241     }
0242 }
0243 
0244 void MastodonMicroBlog::createReply(Choqok::Account *theAccount, MastodonPost *post)
0245 {
0246     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0247     if (acc) {
0248         QVariantMap object;
0249         object.insert(QLatin1String("status"), post->content);
0250 
0251         if (!post->replyToPostId.isEmpty()) {
0252             object.insert(QLatin1String("in_reply_to_id"), post->replyToPostId);
0253         }
0254 
0255         const QByteArray data = QJsonDocument::fromVariant(object).toJson();
0256 
0257         QUrl url(acc->host());
0258         url = url.adjusted(QUrl::StripTrailingSlash);
0259         url.setPath(url.path() + QLatin1String("/api/v1/statuses"));
0260         KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo);
0261         job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json"));
0262         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0263         if (!job) {
0264             qCDebug(CHOQOK) << "Cannot create an http POST request!";
0265             return;
0266         }
0267         m_accountJobs[job] = acc;
0268         m_createPostJobs[job] = post;
0269         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost);
0270         job->start();
0271     } else {
0272         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0273     }
0274 }
0275 
0276 void MastodonMicroBlog::toggleReblog(Choqok::Account *theAccount, Choqok::Post *post)
0277 {
0278     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0279     if (acc) {
0280         QUrl url(acc->host());
0281         url = url.adjusted(QUrl::StripTrailingSlash);
0282         if (acc->username().compare(post->repeatedFromUser.userName) == 0) {
0283             url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unreblog").arg(post->postId));
0284         } else {
0285             url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/reblog").arg(post->postId));
0286         }
0287         KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo);
0288         job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json"));
0289         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0290         if (!job) {
0291             qCDebug(CHOQOK) << "Cannot create an http POST request!";
0292             return;
0293         }
0294         m_accountJobs[job] = acc;
0295         m_shareJobs[job] = post;
0296         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotReblog);
0297         job->start();
0298     } else {
0299         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0300     }
0301 }
0302 
0303 void MastodonMicroBlog::slotReblog(KJob *job)
0304 {
0305     qCDebug(CHOQOK);
0306     if (!job) {
0307         qCDebug(CHOQOK) << "Job is null pointer";
0308         return;
0309     }
0310     Choqok::Post *post = m_shareJobs.take(job);
0311     Choqok::Account *theAccount = m_accountJobs.take(job);
0312     if (!post || !theAccount) {
0313         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0314         return;
0315     }
0316     int ret = 1;
0317     if (job->error()) {
0318         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0319     } else {
0320         Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("The post has been shared."));
0321         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0322 
0323         const QJsonDocument json = QJsonDocument::fromJson(j->data());
0324         if (!json.isNull()) {
0325             ret = 0;
0326         } else {
0327             qCDebug(CHOQOK) << "Cannot parse JSON reply";
0328         }
0329     }
0330 
0331     if (ret) {
0332         Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError,
0333                      i18n("Cannot share the post. %1", job->errorString()));
0334     }
0335 }
0336 
0337 void MastodonMicroBlog::toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post)
0338 {
0339     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0340     if (acc) {
0341         QUrl url(acc->host());
0342         url = url.adjusted(QUrl::StripTrailingSlash);
0343 
0344         if (post->isFavorited) {
0345             url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unfavourite").arg(post->postId));
0346         } else {
0347             url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/favourite").arg(post->postId));
0348         }
0349 
0350         KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo);
0351         job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json"));
0352         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0353         if (!job) {
0354             qCDebug(CHOQOK) << "Cannot create an http POST request!";
0355             return;
0356         }
0357         m_accountJobs[job] = acc;
0358         m_favoriteJobs[job] = post;
0359         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFavorite);
0360         job->start();
0361     } else {
0362         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0363     }
0364 }
0365 
0366 void MastodonMicroBlog::slotFavorite(KJob *job)
0367 {
0368     qCDebug(CHOQOK);
0369     if (!job) {
0370         qCDebug(CHOQOK) << "Job is null pointer";
0371         return;
0372     }
0373     Choqok::Post *post = m_favoriteJobs.take(job);
0374     Choqok::Account *theAccount = m_accountJobs.take(job);
0375     if (!post || !theAccount) {
0376         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0377         return;
0378     }
0379     if (job->error()) {
0380         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0381         Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError,
0382                      i18n("Cannot set/unset the post as favorite. %1", job->errorString()));
0383     } else {
0384         post->isFavorited = !post->isFavorited;
0385         Q_EMIT favorite(theAccount, post);
0386     }
0387 }
0388 
0389 void MastodonMicroBlog::setLastTimelineId(Choqok::Account *theAccount,
0390                                           const QString &timeline,
0391                                           const QString &id)
0392 {
0393     m_timelinesLatestIds[theAccount][timeline] = id;
0394 }
0395 
0396 void MastodonMicroBlog::setTimelinesInfo()
0397 {
0398     Choqok::TimelineInfo *t = new Choqok::TimelineInfo;
0399     t->name = i18nc("Timeline Name", "Home");
0400     t->description = i18nc("Timeline description", "You and people you follow");
0401     t->icon = QLatin1String("user-home");
0402     m_timelinesInfos[QLatin1String("Home")] = t;
0403     m_timelinesPaths[QLatin1String("Home")] = homeTimeline;
0404 
0405     t = new Choqok::TimelineInfo;
0406     t->name = i18nc("Timeline Name", "Local");
0407     t->description = i18nc("Timeline description", "Local timeline");
0408     t->icon = QLatin1String("folder-public");
0409     m_timelinesInfos[QLatin1String("Local")] = t;
0410     m_timelinesPaths[QLatin1String("Local")] = publicTimeline;
0411 
0412     t = new Choqok::TimelineInfo;
0413     t->name = i18nc("Timeline Name", "Federated");
0414     t->description = i18nc("Timeline description", "Federated timeline");
0415     t->icon = QLatin1String("folder-remote");
0416     m_timelinesInfos[QLatin1String("Federated")] = t;
0417     m_timelinesPaths[QLatin1String("Federated")] = publicTimeline;
0418 
0419     t = new Choqok::TimelineInfo;
0420     t->name = i18nc("Timeline Name", "Favourites");
0421     t->description = i18nc("Timeline description", "Favourites");
0422     t->icon = QLatin1String("favorites");
0423     m_timelinesInfos[QLatin1String("Favourites")] = t;
0424     m_timelinesPaths[QLatin1String("Favourites")] = favouritesTimeline;
0425 }
0426 
0427 void MastodonMicroBlog::removePost(Choqok::Account *theAccount, Choqok::Post *post)
0428 {
0429     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0430     if (acc) {
0431         QUrl url(acc->host());
0432         url = url.adjusted(QUrl::StripTrailingSlash);
0433         url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1").arg(post->postId));
0434         KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo);
0435         job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json"));
0436         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0437         if (!job) {
0438             qCDebug(CHOQOK) << "Cannot create an http POST request!";
0439             return;
0440         }
0441         m_accountJobs[job] = acc;
0442         m_removePostJobs[job] = post;
0443         connect(job, &KIO::TransferJob::result, this, &MastodonMicroBlog::slotRemovePost);
0444         job->start();
0445     } else {
0446         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0447     }
0448 }
0449 
0450 QList<Choqok::Post * > MastodonMicroBlog::loadTimeline(Choqok::Account *account,
0451                                                 const QString &timelineName)
0452 {
0453     QList< Choqok::Post * > list;
0454     const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(),
0455                              timelineName);
0456     const KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation);
0457     const QStringList tmpList = postsBackup.groupList();
0458 
0459     // don't load old archives
0460     if (tmpList.isEmpty() || !(QDateTime::fromString(tmpList.first()).isValid())) {
0461         return list;
0462     }
0463 
0464     QList<QDateTime> groupList;
0465     for (const QString &str: tmpList) {
0466         groupList.append(QDateTime::fromString(str));
0467     }
0468     std::sort(groupList.begin(), groupList.end());
0469     MastodonPost *st;
0470     for (const QDateTime &datetime: groupList) {
0471         st = new MastodonPost;
0472         KConfigGroup grp(&postsBackup, datetime.toString());
0473         st->creationDateTime = grp.readEntry("creationDateTime", QDateTime::currentDateTime());
0474         st->postId = grp.readEntry("postId", QString());
0475         st->link = grp.readEntry("link", QUrl());
0476         st->content = grp.readEntry("content", QString());
0477         st->source = grp.readEntry("source", QString());
0478         st->isFavorited = grp.readEntry("favorited", false);
0479         st->author.userId = grp.readEntry("authorId", QString());
0480         st->author.userName = grp.readEntry("authorUserName", QString());
0481         st->author.realName = grp.readEntry("authorRealName", QString());
0482         st->author.description = grp.readEntry("authorDescription" , QString());
0483         st->author.profileImageUrl = grp.readEntry("authorProfileImageUrl", QUrl());
0484         st->author.homePageUrl = grp.readEntry("authorHomePageUrl", QUrl());
0485         st->isRead = grp.readEntry("isRead", true);
0486         st->conversationId = grp.readEntry("conversationId", QString());
0487         st->replyToPostId = grp.readEntry("replyToPostId", QString());
0488         st->replyToUser.userId = grp.readEntry("replyToUserId", QString());
0489         st->repeatedFromUser.userId = grp.readEntry("repeatedFromUserId", QString());
0490         st->repeatedFromUser.userName = grp.readEntry("repeatedFromUserName", QString());
0491         st->repeatedFromUser.homePageUrl = grp.readEntry("repeatedFromUserHomePage", QUrl());
0492         st->repeatedPostId = grp.readEntry("repeatedPostId", QString());
0493         st->repeatedDateTime = grp.readEntry("repeatedDateTime", QDateTime());
0494 
0495         list.append(st);
0496     }
0497 
0498     if (!list.isEmpty()) {
0499         setLastTimelineId(account, timelineName, list.last()->conversationId);
0500     }
0501 
0502     return list;
0503 }
0504 
0505 QUrl MastodonMicroBlog::profileUrl(Choqok::Account *account, const QString &username) const
0506 {
0507     if (username.contains(QLatin1Char('@'))) {
0508         return QUrl::fromUserInput(QStringLiteral("https://%1/@%2").arg(hostFromAcct(username)).arg(userNameFromAcct(username)));
0509     } else {
0510         MastodonAccount *acc = qobject_cast<MastodonAccount *>(account);
0511         QUrl url(acc->host());
0512         url = url.adjusted(QUrl::StripTrailingSlash);
0513         url.setPath(QLatin1String("/@") + username);
0514 
0515         return url;
0516     }
0517 }
0518 
0519 QString MastodonMicroBlog::generateRepeatedByUserTooltip(const QString &username) const
0520 {
0521     if (Choqok::AppearanceSettings::showRetweetsInChoqokWay()) {
0522         return i18n("Boost of %1", username);
0523     } else {
0524         return i18n("Boosted by %1", username);
0525     }
0526 }
0527 
0528 void MastodonMicroBlog::showDirectMessageDialog(MastodonAccount *theAccount, const QString &toUsername)
0529 {
0530     qCDebug(CHOQOK);
0531     if (!theAccount) {
0532         QAction *act = qobject_cast<QAction *>(sender());
0533         theAccount = qobject_cast<MastodonAccount *>(
0534                          Choqok::AccountManager::self()->findAccount(act->data().toString()));
0535     }
0536     MastodonDMessageDialog *dmsg = new MastodonDMessageDialog(theAccount, Choqok::UI::Global::mainWindow());
0537     if (!toUsername.isEmpty()) {
0538         dmsg->setTo(toUsername);
0539     }
0540     dmsg->show();
0541 }
0542 
0543 QString MastodonMicroBlog::hostFromAcct(const QString &acct)
0544 {
0545     if (acct.contains(QLatin1Char('@'))) {
0546         return acct.split(QLatin1Char('@'))[1];
0547     } else {
0548         return acct;
0549     }
0550 }
0551 
0552 QString MastodonMicroBlog::userNameFromAcct(const QString &acct)
0553 {
0554     if (acct.contains(QLatin1Char('@'))) {
0555         return acct.split(QLatin1Char('@'))[0];
0556     } else {
0557         return acct;
0558     }
0559 }
0560 
0561 void MastodonMicroBlog::saveTimeline(Choqok::Account *account, const QString &timelineName,
0562                                      const QList< Choqok::UI::PostWidget * > &timeline)
0563 {
0564     const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(),
0565                              timelineName);
0566     KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation);
0567 
0568     ///Clear previous data:
0569     for (const QString &group: postsBackup.groupList()) {
0570         postsBackup.deleteGroup(group);
0571     }
0572 
0573     for (Choqok::UI::PostWidget *wd: timeline) {
0574         MastodonPost *post = dynamic_cast<MastodonPost * >(wd->currentPost());
0575         KConfigGroup grp(&postsBackup, post->creationDateTime.toString());
0576         grp.writeEntry("creationDateTime", post->creationDateTime);
0577         grp.writeEntry("postId", post->postId);
0578         grp.writeEntry("link", post->link);
0579         grp.writeEntry("content", post->content);
0580         grp.writeEntry("source", post->source);
0581         grp.writeEntry("favorited", post->isFavorited);
0582         grp.writeEntry("authorId", post->author.userId);
0583         grp.writeEntry("authorRealName", post->author.realName);
0584         grp.writeEntry("authorUserName", post->author.userName);
0585         grp.writeEntry("authorDescription", post->author.description);
0586         grp.writeEntry("authorProfileImageUrl", post->author.profileImageUrl);
0587         grp.writeEntry("authorHomePageUrl", post->author.homePageUrl);
0588         grp.writeEntry("isRead", post->isRead);
0589         grp.writeEntry("conversationId", post->conversationId);
0590         grp.writeEntry("replyToPostId", post->replyToPostId);
0591         grp.writeEntry("replyToUserId", post->replyToUser.userId);
0592         grp.writeEntry("repeatedFromUserId", post->repeatedFromUser.userId);
0593         grp.writeEntry("repeatedFromUserName", post->repeatedFromUser.userName);
0594         grp.writeEntry("repeatedFromUserHomePage", post->repeatedFromUser.homePageUrl);
0595         grp.writeEntry("repeatedPostId", post->repeatedPostId);
0596         grp.writeEntry("repeatedDateTime", post->repeatedDateTime);
0597     }
0598     postsBackup.sync();
0599 
0600     if (Choqok::Application::isShuttingDown()) {
0601         --d->countOfTimelinesToSave;
0602         if (d->countOfTimelinesToSave < 1) {
0603             Q_EMIT readyForUnload();
0604         }
0605     }
0606 }
0607 
0608 Choqok::TimelineInfo *MastodonMicroBlog::timelineInfo(const QString &timelineName)
0609 {
0610     return m_timelinesInfos.value(timelineName);
0611 }
0612 
0613 void MastodonMicroBlog::updateTimelines(Choqok::Account *theAccount)
0614 {
0615     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0616     if (acc) {
0617         for (const QString &timeline: acc->timelineNames()) {
0618             QUrl url(acc->host());
0619             url = url.adjusted(QUrl::StripTrailingSlash);
0620             url.setPath(url.path() + QLatin1Char('/') + m_timelinesPaths[timeline]);
0621 
0622             QUrlQuery query;
0623             if (timeline.compare(QLatin1String("Local")) == 0) {
0624                 query.addQueryItem(QLatin1String("local"), QLatin1String("true"));
0625             }
0626             url.setQuery(query);
0627 
0628             KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
0629             if (!job) {
0630                 qCDebug(CHOQOK) << "Cannot create an http GET request!";
0631                 continue;
0632             }
0633             job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0634             m_timelinesRequests[job] = timeline;
0635             m_accountJobs[job] = acc;
0636             connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotUpdateTimeline);
0637             job->start();
0638         }
0639     } else {
0640         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0641     }
0642 }
0643 
0644 QString MastodonMicroBlog::authorizationMetaData(MastodonAccount *account) const
0645 {
0646     return QStringLiteral("Authorization: Bearer ") + account->oAuth()->token();
0647 }
0648 
0649 Choqok::UI::PostWidget *MastodonMicroBlog::createPostWidget(Choqok::Account *account,
0650         Choqok::Post *post,
0651         QWidget *parent)
0652 {
0653     return new MastodonPostWidget(account, post, parent);
0654 }
0655 
0656 void MastodonMicroBlog::fetchPost(Choqok::Account *theAccount, Choqok::Post *post)
0657 {
0658     MastodonAccount *acc = qobject_cast<MastodonAccount *>(theAccount);
0659     if (acc) {
0660         if (!post->link.toDisplayString().startsWith(acc->host())) {
0661             qCDebug(CHOQOK) << "You can only fetch posts from your host!";
0662             return;
0663         }
0664         QUrl url(post->link);
0665 
0666         KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
0667         if (!job) {
0668             qCDebug(CHOQOK) << "Cannot create an http GET request!";
0669             return;
0670         }
0671         job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc));
0672         m_accountJobs[job] = acc;
0673         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFetchPost);
0674         job->start();
0675     } else {
0676         qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!";
0677     }
0678 }
0679 
0680 void MastodonMicroBlog::slotCreatePost(KJob *job)
0681 {
0682     qCDebug(CHOQOK);
0683     if (!job) {
0684         qCDebug(CHOQOK) << "Job is null pointer";
0685         return;
0686     }
0687     Choqok::Post *post = m_createPostJobs.take(job);
0688     Choqok::Account *theAccount = m_accountJobs.take(job);
0689     if (!post || !theAccount) {
0690         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0691         return;
0692     }
0693     int ret = 1;
0694     if (job->error()) {
0695         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0696     } else {
0697         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0698 
0699         const QJsonDocument json = QJsonDocument::fromJson(j->data());
0700         if (!json.isNull()) {
0701             const QVariantMap reply = json.toVariant().toMap();
0702             if (!reply[QLatin1String("id")].toString().isEmpty()) {
0703                 Choqok::NotifyManager::success(i18n("New post for account %1 submitted successfully.",
0704                                                     theAccount->alias()));
0705                 ret = 0;
0706                 Q_EMIT postCreated(theAccount, post);
0707             }
0708         } else {
0709             qCDebug(CHOQOK) << "Cannot parse JSON reply";
0710         }
0711     }
0712 
0713     if (ret) {
0714         Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError,
0715                          i18n("Creating the new post failed. %1", job->errorString()),
0716                          MicroBlog::Critical);
0717     }
0718 }
0719 
0720 void MastodonMicroBlog::slotFetchPost(KJob *job)
0721 {
0722     qCDebug(CHOQOK);
0723     if (!job) {
0724         qCDebug(CHOQOK) << "Job is null pointer";
0725         return;
0726     }
0727     Choqok::Account *theAccount = m_accountJobs.take(job);
0728     if (!theAccount) {
0729         qCDebug(CHOQOK) << "Account or postId is NULL pointer";
0730         return;
0731     }
0732     int ret = 1;
0733     if (job->error()) {
0734         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0735     } else {
0736         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0737 
0738         const QJsonDocument json = QJsonDocument::fromJson(j->data());
0739         if (!json.isNull()) {
0740             const QVariantMap reply = json.toVariant().toMap();
0741             MastodonPost *post = new MastodonPost;
0742             readPost(reply, post);
0743             ret = 0;
0744             Q_EMIT postFetched(theAccount, post);
0745         } else {
0746             qCDebug(CHOQOK) << "Cannot parse JSON reply";
0747         }
0748     }
0749 
0750     if (ret) {
0751         Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError,
0752                      i18n("Cannot fetch post. %1", job->errorString()),
0753                      MicroBlog::Critical);
0754     }
0755 }
0756 
0757 void MastodonMicroBlog::slotRemovePost(KJob *job)
0758 {
0759     qCDebug(CHOQOK);
0760     if (!job) {
0761         qCDebug(CHOQOK) << "Job is null pointer";
0762         return;
0763     }
0764     Choqok::Post *post = m_removePostJobs.take(job);
0765     Choqok::Account *theAccount = m_accountJobs.take(job);
0766     if (!post || !theAccount) {
0767         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0768         return;
0769     }
0770     int ret = 1;
0771     if (job->error()) {
0772         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0773     } else {
0774         KIO::TransferJob *j = qobject_cast<KIO::TransferJob * >(job);
0775 
0776         if (j->metaData().contains(QStringLiteral("responsecode"))) {
0777             int responseCode = j->queryMetaData(QStringLiteral("responsecode")).toInt();
0778 
0779             if (responseCode == 200 || responseCode == 404) {
0780                 ret = 0;
0781                 Q_EMIT postRemoved(theAccount, post);
0782             }
0783         }
0784     }
0785 
0786     if (ret) {
0787         Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError,
0788                          i18n("Removing the post failed. %1", job->errorString()),
0789                          MicroBlog::Critical);
0790     }
0791 }
0792 
0793 void MastodonMicroBlog::slotUpdateTimeline(KJob *job)
0794 {
0795     qCDebug(CHOQOK);
0796     if (!job) {
0797         qCDebug(CHOQOK) << "Job is null pointer";
0798         return;
0799     }
0800     Choqok::Account *account = m_accountJobs.take(job);
0801     if (!account) {
0802         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0803         return;
0804     }
0805     if (job->error()) {
0806         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0807         Q_EMIT error(account, Choqok::MicroBlog::CommunicationError,
0808                      i18n("An error occurred when fetching the timeline"));
0809     } else {
0810         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0811         const QList<Choqok::Post * > list = readTimeline(j->data());
0812         const QString timeline(m_timelinesRequests.take(job));
0813         if (!list.isEmpty()) {
0814             setLastTimelineId(account, timeline, list.last()->conversationId);
0815         }
0816 
0817         Q_EMIT timelineDataReceived(account, timeline, list);
0818     }
0819 }
0820 
0821 void MastodonMicroBlog::fetchFollowers(MastodonAccount* theAccount, bool active)
0822 {
0823     qCDebug(CHOQOK);
0824     QUrl url(theAccount->host());
0825     url = url.adjusted(QUrl::StripTrailingSlash);
0826     url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/followers").arg(theAccount->id()));
0827 
0828     QUrlQuery urlQuery;
0829     urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80"));
0830     url.setQuery(urlQuery);
0831 
0832     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
0833     if (!job) {
0834         qCDebug(CHOQOK) << "Cannot create an http GET request!";
0835         return;
0836     }
0837     job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(theAccount));
0838     mJobsAccount[job] = theAccount;
0839     if (active) {
0840         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNameActive);
0841     } else {
0842         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNamePassive);
0843     }
0844     job->start();
0845     Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating followers list for account %1...",
0846                                                              theAccount->alias()));
0847 }
0848 
0849 void MastodonMicroBlog::slotRequestFollowersScreenNameActive(KJob* job)
0850 {
0851     finishRequestFollowersScreenName(job, true);
0852 }
0853 
0854 void MastodonMicroBlog::slotRequestFollowersScreenNamePassive(KJob* job)
0855 {
0856     finishRequestFollowersScreenName(job, false);
0857 }
0858 
0859 void MastodonMicroBlog::finishRequestFollowersScreenName(KJob *job, bool active)
0860 {
0861     qCDebug(CHOQOK);
0862     if (!job) {
0863         qCDebug(CHOQOK) << "Job is null pointer";
0864         return;
0865     }
0866     Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low;
0867     MastodonAccount *account = qobject_cast<MastodonAccount *>(mJobsAccount.take(job));
0868     if (!account) {
0869         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0870         return;
0871     }
0872 
0873     if (job->error()) {
0874         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0875         Q_EMIT error(account, ServerError, i18n("Followers list for account %1 could not be updated:\n%2",
0876             account->username(), job->errorString()), level);
0877         return;
0878     } else {
0879         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0880 
0881         const QByteArray buffer = j->data();
0882         const QJsonDocument json = QJsonDocument::fromJson(buffer);
0883         if (!json.isNull()) {
0884             QStringList followers;
0885             for (const QVariant &user: json.array().toVariantList()) {
0886                 followers.append(user.toMap()[QLatin1String("acct")].toString());
0887             }
0888 
0889             account->setFollowers(followers);
0890         } else {
0891             QString err = i18n("Retrieving the followers list failed. The data returned from the server is corrupted.");
0892             qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer;
0893             Q_EMIT error(account, ParsingError, err, Critical);
0894         }
0895     }
0896 }
0897 
0898 void MastodonMicroBlog::fetchFollowing(MastodonAccount* theAccount, bool active)
0899 {
0900     qCDebug(CHOQOK);
0901     QUrl url(theAccount->host());
0902     url = url.adjusted(QUrl::StripTrailingSlash);
0903     url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/following").arg(theAccount->id()));
0904 
0905     QUrlQuery urlQuery;
0906     urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80"));
0907     url.setQuery(urlQuery);
0908 
0909     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
0910     if (!job) {
0911         qCDebug(CHOQOK) << "Cannot create an http GET request!";
0912         return;
0913     }
0914     job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(theAccount));
0915     mJobsAccount[job] = theAccount;
0916     if (active) {
0917         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNameActive);
0918     } else {
0919         connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNamePassive);
0920     }
0921     job->start();
0922     Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating following list for account %1...",
0923                                                              theAccount->alias()));
0924 }
0925 
0926 void MastodonMicroBlog::slotRequestFollowingScreenNameActive(KJob* job)
0927 {
0928     finishRequestFollowingScreenName(job, true);
0929 }
0930 
0931 void MastodonMicroBlog::slotRequestFollowingScreenNamePassive(KJob* job)
0932 {
0933     finishRequestFollowingScreenName(job, false);
0934 }
0935 
0936 void MastodonMicroBlog::finishRequestFollowingScreenName(KJob *job, bool active)
0937 {
0938     qCDebug(CHOQOK);
0939     if (!job) {
0940         qCDebug(CHOQOK) << "Job is null pointer";
0941         return;
0942     }
0943     Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low;
0944     MastodonAccount *account = qobject_cast<MastodonAccount *>(mJobsAccount.take(job));
0945     if (!account) {
0946         qCDebug(CHOQOK) << "Account or Post is NULL pointer";
0947         return;
0948     }
0949 
0950     if (job->error()) {
0951         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
0952         Q_EMIT error(account, ServerError, i18n("Following list for account %1 could not be updated:\n%2",
0953             account->username(), job->errorString()), level);
0954         return;
0955     } else {
0956         KIO::StoredTransferJob *j = qobject_cast<KIO::StoredTransferJob * >(job);
0957 
0958         const QByteArray buffer = j->data();
0959         const QJsonDocument json = QJsonDocument::fromJson(buffer);
0960         if (!json.isNull()) {
0961             QStringList following;
0962             for (const QVariant &user: json.array().toVariantList()) {
0963                 following.append(user.toMap()[QLatin1String("acct")].toString());
0964             }
0965 
0966             account->setFollowing(following);
0967         } else {
0968             QString err = i18n("Retrieving the following list failed. The data returned from the server is corrupted.");
0969             qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer;
0970             Q_EMIT error(account, ParsingError, err, Critical);
0971         }
0972     }
0973 }
0974 
0975 #include "mastodonmicroblog.moc"
0976 #include "moc_mastodonmicroblog.cpp"