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"