diff --git a/microblogs/CMakeLists.txt b/microblogs/CMakeLists.txt index fbf153c8..2b4d3750 100644 --- a/microblogs/CMakeLists.txt +++ b/microblogs/CMakeLists.txt @@ -1,13 +1,14 @@ find_package(KF5Attica) add_subdirectory(twitter) +add_subdirectory(mastodon) add_subdirectory(laconica) add_subdirectory(friendica) add_subdirectory(pumpio) IF(KF5Attica_FOUND) message(STATUS "FOUND LibAttica: Will build \"Open Collaboration Services\" plugin") add_subdirectory(ocs) ELSE(KF5Attica_FOUND) message(WARNING "Optional dependency \"LibAttica\" NOT FOUND, won't build \"Open Collaboration Services\" plugin") ENDIF(KF5Attica_FOUND) diff --git a/microblogs/mastodon/CMakeLists.txt b/microblogs/mastodon/CMakeLists.txt new file mode 100644 index 00000000..5b6e6139 --- /dev/null +++ b/microblogs/mastodon/CMakeLists.txt @@ -0,0 +1,41 @@ +include_directories( + ${CHOQOK_INCLUDES} +) + +set(choqok_mastodon_SRCS + mastodonaccount.cpp + mastodondebug.cpp + mastodoneditaccountwidget.cpp + mastodonmicroblog.cpp + mastodonoauth.cpp + mastodonoauthreplyhandler.cpp + mastodonpost.cpp + mastodonpostwidget.cpp +) + +ki18n_wrap_ui(choqok_mastodon_SRCS + mastodoneditaccountwidget.ui +) + +add_library(choqok_mastodon MODULE ${choqok_mastodon_SRCS}) + +kcoreaddons_desktop_to_json(choqok_mastodon choqok_mastodon.desktop) + +target_link_libraries(choqok_mastodon +PUBLIC + Qt5::Core + Qt5::Gui + Qt5::NetworkAuth + Qt5::Widgets + KF5::I18n + KF5::KIOCore + KF5::KIOWidgets + KF5::WidgetsAddons + qca-qt5 + choqok +) + +install(TARGETS choqok_mastodon DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES choqok_mastodon.desktop DESTINATION ${SERVICES_INSTALL_DIR}) + +add_subdirectory(icons) diff --git a/microblogs/mastodon/choqok_mastodon.desktop b/microblogs/mastodon/choqok_mastodon.desktop new file mode 100644 index 00000000..341a5f61 --- /dev/null +++ b/microblogs/mastodon/choqok_mastodon.desktop @@ -0,0 +1,18 @@ +[Desktop Entry] +Encoding=UTF-8 +Type=Service +X-Choqok-Version=1 +Icon=mastodon_microblog +ServiceTypes=Choqok/Plugin +X-KDE-Library=choqok_mastodon +X-KDE-PluginInfo-Author=Andrea Scarpino +X-KDE-PluginInfo-Email=scarpino@kde.org +X-KDE-PluginInfo-Name=choqok_mastodon +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://choqok.gnufolks.org +X-KDE-PluginInfo-Category=MicroBlogs +X-KDE-PluginInfo-Depends= +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Name=Mastodon social +Comment=Mastodon social diff --git a/microblogs/mastodon/icons/128-apps-mastodon_microblog.png b/microblogs/mastodon/icons/128-apps-mastodon_microblog.png new file mode 100644 index 00000000..2058c3db Binary files /dev/null and b/microblogs/mastodon/icons/128-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/16-apps-mastodon_microblog.png b/microblogs/mastodon/icons/16-apps-mastodon_microblog.png new file mode 100644 index 00000000..6e14a7d3 Binary files /dev/null and b/microblogs/mastodon/icons/16-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/22-apps-mastodon_microblog.png b/microblogs/mastodon/icons/22-apps-mastodon_microblog.png new file mode 100644 index 00000000..205ed9b5 Binary files /dev/null and b/microblogs/mastodon/icons/22-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/32-apps-mastodon_microblog.png b/microblogs/mastodon/icons/32-apps-mastodon_microblog.png new file mode 100644 index 00000000..abc8b077 Binary files /dev/null and b/microblogs/mastodon/icons/32-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/48-apps-mastodon_microblog.png b/microblogs/mastodon/icons/48-apps-mastodon_microblog.png new file mode 100644 index 00000000..7bf4a7a7 Binary files /dev/null and b/microblogs/mastodon/icons/48-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/64-apps-mastodon_microblog.png b/microblogs/mastodon/icons/64-apps-mastodon_microblog.png new file mode 100644 index 00000000..20b52395 Binary files /dev/null and b/microblogs/mastodon/icons/64-apps-mastodon_microblog.png differ diff --git a/microblogs/mastodon/icons/CMakeLists.txt b/microblogs/mastodon/icons/CMakeLists.txt new file mode 100644 index 00000000..54b3eafe --- /dev/null +++ b/microblogs/mastodon/icons/CMakeLists.txt @@ -0,0 +1,8 @@ +ecm_install_icons( ICONS + 16-apps-mastodon_microblog.png + 22-apps-mastodon_microblog.png + 32-apps-mastodon_microblog.png + 48-apps-mastodon_microblog.png + 64-apps-mastodon_microblog.png + 128-apps-mastodon_microblog.png + DESTINATION ${ICON_INSTALL_DIR} ) diff --git a/microblogs/mastodon/mastodonaccount.cpp b/microblogs/mastodon/mastodonaccount.cpp new file mode 100644 index 00000000..e17c69f8 --- /dev/null +++ b/microblogs/mastodon/mastodonaccount.cpp @@ -0,0 +1,130 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodonaccount.h" + +#include + +#include "passwordmanager.h" + +#include "mastodonmicroblog.h" + +class MastodonAccount::Private +{ +public: + QString consumerKey; + QString consumerSecret; + QString host; + QString acct; + QString tokenSecret; + QStringList following; + QVariantList lists; + MastodonOAuth *oAuth; + QStringList timelineNames; +}; + +MastodonAccount::MastodonAccount(MastodonMicroBlog *parent, const QString &alias): + Account(parent, alias), d(new Private) +{ + d->host = configGroup()->readEntry("Host", QString()); + d->acct = configGroup()->readEntry("Acct", QString()); + d->tokenSecret = Choqok::PasswordManager::self()->readPassword(QStringLiteral("%1_tokenSecret").arg(alias)); + d->consumerKey = configGroup()->readEntry("ConsumerKey", QString()); + d->consumerSecret = Choqok::PasswordManager::self()->readPassword(QStringLiteral("%1_consumerSecret").arg(alias)); + d->oAuth = new MastodonOAuth(this); + d->oAuth->setToken(d->tokenSecret); + + setPostCharLimit(500); +} + +MastodonAccount::~MastodonAccount() +{ + d->oAuth->deleteLater(); + delete d; +} + +void MastodonAccount::writeConfig() +{ + configGroup()->writeEntry("Host", d->host); + configGroup()->writeEntry("Acct", d->acct); + configGroup()->writeEntry("ConsumerKey", d->consumerKey); + Choqok::PasswordManager::self()->writePassword(QStringLiteral("%1_consumerSecret").arg(alias()), + d->consumerSecret); + Choqok::PasswordManager::self()->writePassword(QStringLiteral("%1_tokenSecret").arg(alias()), + d->tokenSecret); + Choqok::Account::writeConfig(); +} + +QString MastodonAccount::host() +{ + return d->host; +} + +void MastodonAccount::setHost(const QString &host) +{ + d->host = host; +} + +QString MastodonAccount::acct() +{ + return d->acct; +} + +void MastodonAccount::setAcct(const QString &acct) +{ + d->acct = acct; +} + +QString MastodonAccount::consumerKey() +{ + return d->consumerKey; +} + +void MastodonAccount::setConsumerKey(const QString &consumerKey) +{ + d->consumerKey = consumerKey; +} + +QString MastodonAccount::consumerSecret() +{ + return d->consumerSecret; +} + +void MastodonAccount::setConsumerSecret(const QString &consumerSecret) +{ + d->consumerSecret = consumerSecret; +} + +QString MastodonAccount::tokenSecret() +{ + return d->tokenSecret; +} + +void MastodonAccount::setTokenSecret(const QString &tokenSecret) +{ + d->tokenSecret = tokenSecret; +} + +MastodonOAuth *MastodonAccount::oAuth() +{ + return d->oAuth; +} diff --git a/microblogs/mastodon/mastodonaccount.h b/microblogs/mastodon/mastodonaccount.h new file mode 100644 index 00000000..05dea4eb --- /dev/null +++ b/microblogs/mastodon/mastodonaccount.h @@ -0,0 +1,65 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#ifndef MASTODONACCOUNT_H +#define MASTODONACCOUNT_H + +#include "account.h" +#include "choqoktypes.h" + +#include "mastodonoauth.h" + +class MastodonMicroBlog; + +class MastodonAccount : public Choqok::Account +{ + Q_OBJECT +public: + explicit MastodonAccount(MastodonMicroBlog *parent, const QString &alias); + ~MastodonAccount(); + + virtual void writeConfig() override; + + QString host(); + void setHost(const QString &host); + + QString acct(); + void setAcct(const QString &acct); + + QString consumerKey(); + void setConsumerKey(const QString &consumerKey); + + QString consumerSecret(); + void setConsumerSecret(const QString &consumerSecret); + + QString tokenSecret(); + void setTokenSecret(const QString &tokenSecret); + + MastodonOAuth *oAuth(); + +private: + class Private; + Private *d; + +}; + +#endif // MASTODONACCOUNT_H diff --git a/microblogs/mastodon/mastodondebug.cpp b/microblogs/mastodon/mastodondebug.cpp new file mode 100644 index 00000000..1acdbe80 --- /dev/null +++ b/microblogs/mastodon/mastodondebug.cpp @@ -0,0 +1,22 @@ +/* This file is part of the KDE project + Copyright (C) 2017 Andrea Scarpino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "mastodondebug.h" +Q_LOGGING_CATEGORY(CHOQOK, "org.kde.choqok.mastodon") + diff --git a/microblogs/mastodon/mastodondebug.h b/microblogs/mastodon/mastodondebug.h new file mode 100644 index 00000000..723b95a6 --- /dev/null +++ b/microblogs/mastodon/mastodondebug.h @@ -0,0 +1,27 @@ +/* This file is part of the KDE project + Copyright (C) 2017 Andrea Scarpino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef MASTODONDEBUG_H +#define MASTODONDEBUG_H + +#include +Q_DECLARE_LOGGING_CATEGORY(CHOQOK) + +#endif + diff --git a/microblogs/mastodon/mastodoneditaccountwidget.cpp b/microblogs/mastodon/mastodoneditaccountwidget.cpp new file mode 100644 index 00000000..07072e77 --- /dev/null +++ b/microblogs/mastodon/mastodoneditaccountwidget.cpp @@ -0,0 +1,221 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodoneditaccountwidget.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "choqoktools.h" +#include "accountmanager.h" + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodonmicroblog.h" +#include "mastodonoauth.h" + +MastodonEditAccountWidget::MastodonEditAccountWidget(MastodonMicroBlog *microblog, + MastodonAccount *account, + QWidget *parent): + ChoqokEditAccountWidget(account, parent) + , m_account(account) +{ + setupUi(this); + + connect(kcfg_authorize, SIGNAL(clicked(bool)), SLOT(authorizeUser())); + + if (m_account) { + kcfg_alias->setText(m_account->alias()); + kcfg_acct->setText(m_account->acct()); + setAuthenticated(!m_account->tokenSecret().isEmpty()); + } else { + setAuthenticated(false); + QString newAccountAlias = microblog->serviceName(); + const QString servName = newAccountAlias; + int counter = 1; + while (Choqok::AccountManager::self()->findAccount(newAccountAlias)) { + newAccountAlias = QStringLiteral("%1%2").arg(servName).arg(counter); + counter++; + } + m_account = new MastodonAccount(microblog, newAccountAlias); + setAccount(m_account); + kcfg_alias->setText(newAccountAlias); + } + + loadTimelinesTable(); +} + +MastodonEditAccountWidget::~MastodonEditAccountWidget() +{ +} + +Choqok::Account *MastodonEditAccountWidget::apply() +{ + m_account->setAlias(kcfg_alias->text()); + m_account->setAcct(kcfg_acct->text()); + m_account->setTokenSecret(m_account->oAuth()->token()); + m_account->writeConfig(); + saveTimelinesTable(); + return m_account; +} + +void MastodonEditAccountWidget::authorizeUser() +{ + qCDebug(CHOQOK); + if (kcfg_acct->text().isEmpty() || !kcfg_acct->text().contains(QLatin1Char('@'))) { + return; + } + if (m_account->consumerKey().isEmpty() || m_account->consumerSecret().isEmpty()) { + registerClient(); + } + + connect(m_account->oAuth(), &QAbstractOAuth::authorizeWithBrowser, &Choqok::openUrl); + connect(m_account->oAuth(), &QAbstractOAuth::statusChanged, this, &MastodonEditAccountWidget::gotToken); + + m_account->oAuth()->grant(); + + QString verifier = QInputDialog::getText(this, i18n("coe"), + i18n("Enter the code received from %1", m_account->host())); + if (verifier.isEmpty()) { + return; + } + + m_account->oAuth()->getToken(verifier); +} + +void MastodonEditAccountWidget::gotToken() +{ + isAuthenticated = false; + if (m_account->oAuth()->status() == QAbstractOAuth::Status::Granted) { + setAuthenticated(true); + KMessageBox::information(this, i18n("Choqok is authorized successfully."), i18n("Authorized")); + } else { + KMessageBox::detailedError(this, i18n("Authorization Error"), i18n("OAuth authorization error")); + } +} + +bool MastodonEditAccountWidget::validateData() +{ + if (kcfg_alias->text().isEmpty() || kcfg_acct->text().isEmpty() || + !kcfg_acct->text().contains(QLatin1Char('@')) || + !isAuthenticated) { + return false; + } else { + return true; + } +} + +void MastodonEditAccountWidget::setAuthenticated(bool authenticated) +{ + isAuthenticated = authenticated; + if (authenticated) { + kcfg_authorize->setIcon(QIcon::fromTheme(QLatin1String("object-unlocked"))); + kcfg_authenticateLed->on(); + kcfg_authenticateStatus->setText(i18n("Authenticated")); + } else { + kcfg_authorize->setIcon(QIcon::fromTheme(QLatin1String("object-locked"))); + kcfg_authenticateLed->off(); + kcfg_authenticateStatus->setText(i18n("Not Authenticated")); + } +} + +void MastodonEditAccountWidget::loadTimelinesTable() +{ + for (const QString &timeline: m_account->microblog()->timelineNames()) { + int newRow = timelinesTable->rowCount(); + timelinesTable->insertRow(newRow); + timelinesTable->setItem(newRow, 0, new QTableWidgetItem(timeline)); + + QCheckBox *enable = new QCheckBox(timelinesTable); + enable->setChecked(m_account->timelineNames().contains(timeline)); + timelinesTable->setCellWidget(newRow, 1, enable); + } +} + +void MastodonEditAccountWidget::registerClient() +{ + if (kcfg_acct->text().contains(QLatin1Char('@'))) { + m_account->setUsername(kcfg_acct->text().split(QLatin1Char('@'))[0]); + m_account->setHost(QLatin1String("https://") + kcfg_acct->text().split(QLatin1Char('@'))[1]); + + m_account->oAuth()->setAccessTokenUrl(QUrl(m_account->host() + QLatin1String("/oauth/token"))); + m_account->oAuth()->setAuthorizationUrl(QUrl(m_account->host() + QLatin1String("/oauth/authorize"))); + + QUrl url(m_account->host() + QLatin1String("/api/v1/apps")); + QByteArray data; + data += "client_name=" + QCoreApplication::applicationName().toLatin1(); + data += "&redirect_uris=" + QUrl::toPercentEncoding(QLatin1String("urn:ietf:wg:oauth:2.0:oob")); + data += "&scopes=" + QUrl::toPercentEncoding(QLatin1String("read write follow")); + data += "&website=" + QUrl::toPercentEncoding(QLatin1String("http://choqok.gnufolks.org/")); + + KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/x-www-form-urlencoded")); + QEventLoop loop; + connect(job, SIGNAL(result(KJob*)), &loop, SLOT(quit())); + job->start(); + loop.exec(); + + if (job->error()) { + qCDebug(CHOQOK) << "An error occurred in Job"; + return; + } else { + KIO::StoredTransferJob *stj = qobject_cast(job); + + const QJsonDocument json = QJsonDocument::fromJson(stj->data()); + if (!json.isNull()) { + const QVariantMap result = json.toVariant().toMap(); + m_account->setConsumerKey(result[QLatin1String("client_id")].toString()); + m_account->setConsumerSecret(result[QLatin1String("client_secret")].toString()); + m_account->oAuth()->setClientIdentifier(m_account->consumerKey()); + m_account->oAuth()->setClientIdentifierSharedKey(m_account->consumerSecret()); + } else { + qCDebug(CHOQOK) << "Cannot parse JSON reply"; + } + } + } else { + qCDebug(CHOQOK) << "username is not valid"; + } +} + +void MastodonEditAccountWidget::saveTimelinesTable() +{ + QStringList timelines; + for (int i = 0; i < timelinesTable->rowCount(); ++i) { + QCheckBox *enable = qobject_cast(timelinesTable->cellWidget(i, 1)); + if (enable && enable->isChecked()) { + timelines.append(timelinesTable->item(i, 0)->text()); + } + } + //m_account->setTimelineNames(timelines); +} diff --git a/microblogs/mastodon/mastodoneditaccountwidget.h b/microblogs/mastodon/mastodoneditaccountwidget.h new file mode 100644 index 00000000..aa1d8e73 --- /dev/null +++ b/microblogs/mastodon/mastodoneditaccountwidget.h @@ -0,0 +1,61 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#ifndef MASTODONEDITACCOUNTWIDGET_H +#define MASTODONEDITACCOUNTWIDGET_H + +#include "editaccountwidget.h" + +#include + +#include "ui_mastodoneditaccountwidget.h" + +class MastodonAccount; +class MastodonMicroBlog; + +class MastodonEditAccountWidget : public ChoqokEditAccountWidget, Ui::MastodonEditAccountWidget +{ + Q_OBJECT +public: + explicit MastodonEditAccountWidget(MastodonMicroBlog *microblog, MastodonAccount *account, + QWidget *parent); + ~MastodonEditAccountWidget(); + + virtual Choqok::Account *apply() override; + + virtual bool validateData() override; + +private Q_SLOTS: + void authorizeUser(); + void gotToken(); + +private: + void setAuthenticated(bool authenticated); + void loadTimelinesTable(); + void registerClient(); + void saveTimelinesTable(); + + MastodonAccount *m_account; + bool isAuthenticated; +}; + +#endif // MASTODONEDITACCOUNTWIDGET_H diff --git a/microblogs/mastodon/mastodoneditaccountwidget.ui b/microblogs/mastodon/mastodoneditaccountwidget.ui new file mode 100644 index 00000000..819fc07f --- /dev/null +++ b/microblogs/mastodon/mastodoneditaccountwidget.ui @@ -0,0 +1,225 @@ + + + MastodonEditAccountWidget + + + + 0 + 0 + 361 + 403 + + + + + + + 0 + + + + Mastodon Account + + + + + + A&lias: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_alias + + + + + + + The account alias + + + The alias is the name you want to give to your account. It should be unique. You can have several connections to the same service so the alias lets you give them names. + + + + + + + + 0 + 0 + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'DejaVu Sans'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt; font-weight:600;">Note:</span><span style=" font-size:8pt;"> The alias must be unique.</span></p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Credentials + + + + QFormLayout::ExpandingFieldsGrow + + + + + username@domain: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + OAuth Authentication + + + + + + + 0 + 0 + + + + Click the below button; if everything goes well, you will pointed to Mastodon website to allow access to Choqok. + + + true + + + + + + + true + + + + 0 + 0 + + + + KLed::Off + + + + + + + Not Authenticated + + + + + + + Verify credentials + + + &Authenticate with Mastodon Service + + + + .. + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Timelines Configuration + + + + + + + 75 + true + + + + Which timelines do you like to be enabled? + + + + + + + + Name + + + + + Enable + + + + + + + + + + + + + KLed + QWidget +
kled.h
+
+
+ + kcfg_alias + kcfg_acct + kcfg_authorize + tabwidget + + + +
diff --git a/microblogs/mastodon/mastodonmicroblog.cpp b/microblogs/mastodon/mastodonmicroblog.cpp new file mode 100644 index 00000000..93da4db6 --- /dev/null +++ b/microblogs/mastodon/mastodonmicroblog.cpp @@ -0,0 +1,745 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodonmicroblog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "accountmanager.h" +#include "application.h" +#include "choqokbehaviorsettings.h" +#include "notifymanager.h" +#include "postwidget.h" + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodoneditaccountwidget.h" +#include "mastodonpost.h" +#include "mastodonpostwidget.h" + +class MastodonMicroBlog::Private +{ +public: + Private(): countOfTimelinesToSave(0) + {} + int countOfTimelinesToSave; +}; + +K_PLUGIN_FACTORY_WITH_JSON(MastodonMicroBlogFactory, "choqok_mastodon.json", + registerPlugin < MastodonMicroBlog > ();) + +const QString MastodonMicroBlog::homeTimeline(QLatin1String("/api/v1/timelines/home")); +const QString MastodonMicroBlog::publicTimeline(QLatin1String("/api/v1/timelines/public")); + +MastodonMicroBlog::MastodonMicroBlog(QObject *parent, const QVariantList &args): + MicroBlog(QStringLiteral("Mastodon") , parent), d(new Private) +{ + Q_UNUSED(args) + setServiceName(QLatin1String("Mastodon")); + setServiceHomepageUrl(QLatin1String("https://mastodon.social")); + QStringList timelineNames; + timelineNames << QLatin1String("Home") << QLatin1String("Local") << QLatin1String("Federated"); + setTimelineNames(timelineNames); + setTimelinesInfo(); +} + +MastodonMicroBlog::~MastodonMicroBlog() +{ + qDeleteAll(m_timelinesInfos); + delete d; +} + +void MastodonMicroBlog::aboutToUnload() +{ + for (Choqok::Account *acc: Choqok::AccountManager::self()->accounts()) { + if (acc->microblog() == this) { + d->countOfTimelinesToSave += acc->timelineNames().count(); + } + } + Q_EMIT saveTimelines(); +} + +ChoqokEditAccountWidget *MastodonMicroBlog::createEditAccountWidget(Choqok::Account *account, + QWidget *parent) +{ + MastodonAccount *acc = qobject_cast(account); + if (acc || !account) { + return new MastodonEditAccountWidget(this, acc, parent); + } else { + qCDebug(CHOQOK) << "Account passed here was not a valid MastodonAccount!"; + return 0; + } +} + +void MastodonMicroBlog::createPost(Choqok::Account *theAccount, Choqok::Post *post) +{ + if (!post || post->content.isEmpty()) { + qCDebug(CHOQOK) << "ERROR: Status text is empty!"; + Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::OtherError, + i18n("Creating the new post failed. Text is empty."), MicroBlog::Critical); + return; + } + + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QVariantMap object; + object.insert(QLatin1String("status"), post->content); + + const QByteArray data = QJsonDocument::fromVariant(object).toJson(); + + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QLatin1String("/api/v1/statuses")); + KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + m_accountJobs[job] = acc; + m_createPostJobs[job] = post; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotCreatePost(KJob*))); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +Choqok::Account *MastodonMicroBlog::createNewAccount(const QString &alias) +{ + MastodonAccount *acc = qobject_cast( + Choqok::AccountManager::self()->findAccount(alias)); + if (!acc) { + return new MastodonAccount(this, alias); + } else { + qCDebug(CHOQOK) << "Cannot create a new MastodonAccount!"; + return 0; + } +} + +QString MastodonMicroBlog::lastTimelineId(Choqok::Account *theAccount, + const QString &timeline) const +{ + qCDebug(CHOQOK) << "Latest ID for timeline " << timeline << m_timelinesLatestIds[theAccount][timeline]; + return m_timelinesLatestIds[theAccount][timeline]; +} + +QList< Choqok::Post * > MastodonMicroBlog::readTimeline(const QByteArray &buffer) +{ + QList posts; + const QJsonDocument json = QJsonDocument::fromJson(buffer); + if (!json.isNull()) { + const QVariantList list = json.array().toVariantList(); + for (const QVariant &element: list) { + posts.prepend(readPost(element.toMap(), new MastodonPost)); + } + } else { + qCDebug(CHOQOK) << "Cannot parse JSON reply"; + } + + return posts; +} + +Choqok::Post *MastodonMicroBlog::readPost(const QVariantMap &var, Choqok::Post *post) +{ + MastodonPost *p = dynamic_cast< MastodonPost * >(post); + if (p) { + QVariantMap reblog = var[QLatin1String("reblog")].toMap(); + QVariantMap status; + if (reblog.isEmpty()) { + status = var; + } else { + status = reblog; + } + + QTextDocument content; + content.setHtml(status[QLatin1String("spoiler_text")].toString() + QLatin1String("
") + status[QLatin1String("content")].toString()); + p->content += content.toPlainText().trimmed(); + + p->creationDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(), + Qt::ISODate); + p->creationDateTime.setTimeSpec(Qt::UTC); + + p->link = var[QLatin1String("url")].toString(); + p->isFavorited = var[QLatin1String("favourited")].toBool(); + if (p->isFavorited) { + p->isRead = true; + } + p->postId = var[QLatin1String("id")].toString(); + + p->conversationId = var[QLatin1String("id")].toString(); + + QVariantMap application = var[QLatin1String("application")].toMap(); + if (!application.isEmpty()) { + p->source = application[QLatin1String("name")].toString(); + } + + if (var[QLatin1String("visibility")].toString().compare(QLatin1String("direct")) == 0) { + p->isPrivate = true; + } + + QVariantMap account = status[QLatin1Literal("account")].toMap(); + + p->author.userId = account[QLatin1String("acct")].toString(); + p->author.userName = account[QLatin1String("username")].toString(); + p->author.realName = account[QLatin1String("display_name")].toString(); + p->author.homePageUrl = account[QLatin1String("url")].toString(); + p->author.followersCount = account[QLatin1String("followers_count")].toUInt(); + + QTextDocument description; + description.setHtml(account[QLatin1String("note")].toString()); + p->author.description = description.toPlainText().trimmed(); + + p->author.profileImageUrl = account[QLatin1String("avatar")].toString(); + + p->replyToPostId = var[QLatin1String("in_reply_to_id")].toString(); + p->replyToUserId = var[QLatin1String("in_reply_to_account_id")].toString(); + + if (!reblog.isEmpty()) { + p->repeatedDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(), + Qt::ISODate); + p->repeatedDateTime.setTimeSpec(Qt::UTC); + + p->repeatedPostId = var[QLatin1String("id")].toString(); + p->repeatedFromUsername = var[QLatin1Literal("account")].toMap()[QLatin1String("acct")].toString(); + } + + return p; + } else { + qCDebug(CHOQOK) << "post is not a MastodonPost!"; + return post; + } +} + +void MastodonMicroBlog::toggleReblog(Choqok::Account *theAccount, Choqok::Post *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + if (acc->username().compare(post->repeatedFromUsername) == 0) { + url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unreblog").arg(post->postId)); + } else { + url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/reblog").arg(post->postId)); + } + KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo); + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + m_accountJobs[job] = acc; + m_shareJobs[job] = post; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotReblog(KJob*))); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +void MastodonMicroBlog::slotReblog(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Post *post = m_shareJobs.take(job); + Choqok::Account *theAccount = m_accountJobs.take(job); + if (!post || !theAccount) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + int ret = 1; + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + } else { + Choqok::UI::Global::mainWindow()->showStatusMessage( + i18n("The post has been shared.")); + KIO::StoredTransferJob *j = qobject_cast(job); + + const QJsonDocument json = QJsonDocument::fromJson(j->data()); + if (!json.isNull()) { + ret = 0; + } else { + qCDebug(CHOQOK) << "Cannot parse JSON reply"; + } + } + + if (ret) { + Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, + i18n("Cannot share the post. %1", job->errorString())); + } +} + +void MastodonMicroBlog::toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + + if (post->isFavorited) { + url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unfavourite").arg(post->postId)); + } else { + url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/favourite").arg(post->postId)); + } + + KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo); + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + m_accountJobs[job] = acc; + m_favoriteJobs[job] = post; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotFavorite(KJob*))); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +void MastodonMicroBlog::slotFavorite(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Post *post = m_favoriteJobs.take(job); + Choqok::Account *theAccount = m_accountJobs.take(job); + if (!post || !theAccount) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, + i18n("Cannot set/unset the post as favorite. %1", job->errorString())); + } else { + post->isFavorited = !post->isFavorited; + Q_EMIT favorite(theAccount, post); + } +} + +void MastodonMicroBlog::setLastTimelineId(Choqok::Account *theAccount, + const QString &timeline, + const QString &id) +{ + m_timelinesLatestIds[theAccount][timeline] = id; +} + +void MastodonMicroBlog::setTimelinesInfo() +{ + Choqok::TimelineInfo *t = new Choqok::TimelineInfo; + t->name = i18nc("Timeline Name", "Home"); + t->description = i18nc("Timeline description", "You and people you follow"); + t->icon = QLatin1String("user-home"); + m_timelinesInfos[QLatin1String("Home")] = t; + m_timelinesPaths[QLatin1String("Home")] = homeTimeline; + + t = new Choqok::TimelineInfo; + t->name = i18nc("Timeline Name", "Local"); + t->description = i18nc("Timeline description", "Local timeline"); + t->icon = QLatin1String("folder-public"); + m_timelinesInfos[QLatin1String("Local")] = t; + m_timelinesPaths[QLatin1String("Local")] = publicTimeline; + + t = new Choqok::TimelineInfo; + t->name = i18nc("Timeline Name", "Federated"); + t->description = i18nc("Timeline description", "Federated timelime"); + t->icon = QLatin1String("folder-remote"); + m_timelinesInfos[QLatin1String("Federated")] = t; + m_timelinesPaths[QLatin1String("Federated")] = publicTimeline; +} + +void MastodonMicroBlog::removePost(Choqok::Account *theAccount, Choqok::Post *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1").arg(post->postId)); + KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo); + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + m_accountJobs[job] = acc; + m_removePostJobs[job] = post; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotRemovePost(KJob*))); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +QList MastodonMicroBlog::loadTimeline(Choqok::Account *account, + const QString &timelineName) +{ + QList< Choqok::Post * > list; + const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(), + timelineName); + const KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation); + const QStringList tmpList = postsBackup.groupList(); + + // don't load old archives + if (tmpList.isEmpty() || !(QDateTime::fromString(tmpList.first()).isValid())) { + return list; + } + + QList groupList; + for (const QString &str: tmpList) { + groupList.append(QDateTime::fromString(str)); + } + qSort(groupList); + MastodonPost *st; + for (const QDateTime &datetime: groupList) { + st = new MastodonPost; + KConfigGroup grp(&postsBackup, datetime.toString()); + st->creationDateTime = grp.readEntry("creationDateTime", QDateTime::currentDateTime()); + st->postId = grp.readEntry("postId", QString()); + st->link = grp.readEntry("link", QString()); + st->content = grp.readEntry("content", QString()); + st->source = grp.readEntry("source", QString()); + st->isFavorited = grp.readEntry("favorited", false); + st->author.userId = grp.readEntry("authorId", QString()); + st->author.userName = grp.readEntry("authorUserName", QString()); + st->author.realName = grp.readEntry("authorRealName", QString()); + st->author.description = grp.readEntry("authorDescription" , QString()); + st->author.profileImageUrl = grp.readEntry("authorProfileImageUrl", QString()); + st->author.homePageUrl = grp.readEntry("authorHomePageUrl", QString()); + st->isRead = grp.readEntry("isRead", true); + st->conversationId = grp.readEntry("conversationId", QString()); + st->replyToPostId = grp.readEntry("replyToPostId", QString()); + st->replyToUserId = grp.readEntry("replyToUserId", QString()); + st->repeatedFromUsername = grp.readEntry("repeatedFrom", QString()); + st->repeatedPostId = grp.readEntry("repeatedPostId", QString()); + st->repeatedDateTime = grp.readEntry("repeatedDateTime", QDateTime()); + + list.append(st); + } + + if (!list.isEmpty()) { + setLastTimelineId(account, timelineName, list.last()->conversationId); + } + + return list; +} + +QUrl MastodonMicroBlog::profileUrl(Choqok::Account *account, const Choqok::User &user) const +{ + Q_UNUSED(account) + return QUrl(user.homePageUrl); +} + +QString MastodonMicroBlog::hostFromAcct(const QString &acct) +{ + if (acct.contains(QLatin1Char('@'))) { + return acct.split(QLatin1Char('@'))[1]; + } else { + return acct; + } +} + +QString MastodonMicroBlog::userNameFromAcct(const QString &acct) +{ + if (acct.contains(QLatin1Char('@'))) { + return acct.split(QLatin1Char('@'))[0]; + } else { + return acct; + } +} + +void MastodonMicroBlog::saveTimeline(Choqok::Account *account, const QString &timelineName, + const QList< Choqok::UI::PostWidget * > &timeline) +{ + const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(), + timelineName); + KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation); + + ///Clear previous data: + for (const QString &group: postsBackup.groupList()) { + postsBackup.deleteGroup(group); + } + + for (Choqok::UI::PostWidget *wd: timeline) { + MastodonPost *post = dynamic_cast(wd->currentPost()); + KConfigGroup grp(&postsBackup, post->creationDateTime.toString()); + grp.writeEntry("creationDateTime", post->creationDateTime); + grp.writeEntry("postId", post->postId); + grp.writeEntry("link", post->link); + grp.writeEntry("content", post->content); + grp.writeEntry("source", post->source); + grp.writeEntry("favorited", post->isFavorited); + grp.writeEntry("authorId", post->author.userId); + grp.writeEntry("authorRealName", post->author.realName); + grp.writeEntry("authorUserName", post->author.userName); + grp.writeEntry("authorDescription", post->author.description); + grp.writeEntry("authorProfileImageUrl", post->author.profileImageUrl); + grp.writeEntry("authorHomePageUrl", post->author.homePageUrl); + grp.writeEntry("isRead", post->isRead); + grp.writeEntry("conversationId", post->conversationId); + grp.writeEntry("replyToPostId", post->replyToPostId); + grp.writeEntry("replyToUserId", post->replyToUserId); + grp.writeEntry("repeatedFrom", post->repeatedFromUsername); + grp.writeEntry("repeatedPostId", post->repeatedPostId); + grp.writeEntry("repeatedDateTime", post->repeatedDateTime); + } + postsBackup.sync(); + + if (Choqok::Application::isShuttingDown()) { + --d->countOfTimelinesToSave; + if (d->countOfTimelinesToSave < 1) { + Q_EMIT readyForUnload(); + } + } +} + +Choqok::TimelineInfo *MastodonMicroBlog::timelineInfo(const QString &timelineName) +{ + return m_timelinesInfos.value(timelineName); +} + +void MastodonMicroBlog::updateTimelines(Choqok::Account *theAccount) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + for (const QString &timeline: acc->timelineNames()) { + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QLatin1Char('/') + m_timelinesPaths[timeline]); + + QUrlQuery query; + if (timeline.compare(QLatin1String("Local")) == 0) { + query.addQueryItem(QLatin1String("local"), QLatin1String("true")); + } + url.setQuery(query); + + KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http GET request!"; + continue; + } + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + m_timelinesRequests[job] = timeline; + m_accountJobs[job] = acc; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotUpdateTimeline(KJob*))); + job->start(); + } + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +QString MastodonMicroBlog::authorizationMetaData(MastodonAccount *account) const +{ + return QStringLiteral("Authorization: Bearer ") + account->oAuth()->token(); +} + +Choqok::UI::PostWidget *MastodonMicroBlog::createPostWidget(Choqok::Account *account, + Choqok::Post *post, + QWidget *parent) +{ + return new MastodonPostWidget(account, post, parent); +} + +void MastodonMicroBlog::fetchPost(Choqok::Account *theAccount, Choqok::Post *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + if (!post->link.startsWith(acc->host())) { + qCDebug(CHOQOK) << "You can only fetch posts from your host!"; + return; + } + QUrl url(post->link); + + KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http GET request!"; + return; + } + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + m_accountJobs[job] = acc; + connect(job, SIGNAL(result(KJob*)), this, SLOT(slotFetchPost(KJob*))); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +void MastodonMicroBlog::slotCreatePost(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Post *post = m_createPostJobs.take(job); + Choqok::Account *theAccount = m_accountJobs.take(job); + if (!post || !theAccount) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + int ret = 1; + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QJsonDocument json = QJsonDocument::fromJson(j->data()); + if (!json.isNull()) { + const QVariantMap reply = json.toVariant().toMap(); + if (!reply[QLatin1String("id")].toString().isEmpty()) { + Choqok::NotifyManager::success(i18n("New post submitted successfully")); + ret = 0; + Q_EMIT postCreated(theAccount, post); + } + } else { + qCDebug(CHOQOK) << "Cannot parse JSON reply"; + } + } + + if (ret) { + Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError, + i18n("Creating the new post failed. %1", job->errorString()), + MicroBlog::Critical); + } +} + +void MastodonMicroBlog::slotFetchPost(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Account *theAccount = m_accountJobs.take(job); + if (!theAccount) { + qCDebug(CHOQOK) << "Account or postId is NULL pointer"; + return; + } + int ret = 1; + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QJsonDocument json = QJsonDocument::fromJson(j->data()); + if (!json.isNull()) { + const QVariantMap reply = json.toVariant().toMap(); + MastodonPost *post = new MastodonPost; + readPost(reply, post); + ret = 0; + Q_EMIT postFetched(theAccount, post); + } else { + qCDebug(CHOQOK) << "Cannot parse JSON reply"; + } + } + + if (ret) { + Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, + i18n("Cannot fetch post. %1", job->errorString()), + MicroBlog::Critical); + } +} + +void MastodonMicroBlog::slotRemovePost(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Post *post = m_removePostJobs.take(job); + Choqok::Account *theAccount = m_accountJobs.take(job); + if (!post || !theAccount) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + int ret = 1; + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + } else { + KIO::TransferJob *j = qobject_cast(job); + + if (j->metaData().contains(QStringLiteral("responsecode"))) { + int responseCode = j->queryMetaData(QStringLiteral("responsecode")).toInt(); + + if (responseCode == 200 || responseCode == 404) { + ret = 0; + Q_EMIT postRemoved(theAccount, post); + } + } + } + + if (ret) { + Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError, + i18n("Removing the post failed. %1", job->errorString()), + MicroBlog::Critical); + } +} + +void MastodonMicroBlog::slotUpdateTimeline(KJob *job) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::Account *account = m_accountJobs.take(job); + if (!account) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + Q_EMIT error(account, Choqok::MicroBlog::CommunicationError, + i18n("An error occurred when fetching the timeline")); + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + const QList list = readTimeline(j->data()); + const QString timeline(m_timelinesRequests.take(job)); + if (!list.isEmpty()) { + setLastTimelineId(account, timeline, list.last()->conversationId); + } + + Q_EMIT timelineDataReceived(account, timeline, list); + } +} + +#include "mastodonmicroblog.moc" diff --git a/microblogs/mastodon/mastodonmicroblog.h b/microblogs/mastodon/mastodonmicroblog.h new file mode 100644 index 00000000..3f489e52 --- /dev/null +++ b/microblogs/mastodon/mastodonmicroblog.h @@ -0,0 +1,120 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#ifndef MASTODONMICROBLOG_H +#define MASTODONMICROBLOG_H + +#include + +#include "microblog.h" + +class QUrl; +class KJob; +class MastodonAccount; +class MastodonPost; + +class MastodonMicroBlog : public Choqok::MicroBlog +{ + Q_OBJECT +public: + explicit MastodonMicroBlog(QObject *parent, const QVariantList &args); + virtual ~MastodonMicroBlog(); + + virtual void aboutToUnload() override; + + virtual ChoqokEditAccountWidget *createEditAccountWidget(Choqok::Account *account, QWidget *parent) override; + + virtual void createPost(Choqok::Account *theAccount, Choqok::Post *post) override; + + virtual Choqok::Account *createNewAccount(const QString &alias) override; + + virtual Choqok::UI::PostWidget *createPostWidget(Choqok::Account *account, + Choqok::Post *post, + QWidget *parent) override; + + virtual void fetchPost(Choqok::Account *theAccount, Choqok::Post *post) override; + + virtual QList loadTimeline(Choqok::Account *account, + const QString &timelineName) override; + + virtual void removePost(Choqok::Account *theAccount, Choqok::Post *post) override; + + virtual QUrl profileUrl(Choqok::Account *account, const Choqok::User &user) const override; + + virtual void saveTimeline(Choqok::Account *account, const QString &timelineName, + const QList< Choqok::UI::PostWidget * > &timeline) override; + + virtual Choqok::TimelineInfo *timelineInfo(const QString &timelineName) override; + + virtual void updateTimelines(Choqok::Account *theAccount) override; + + void toggleReblog(Choqok::Account *theAccount, Choqok::Post *post); + + void toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post); + + static QString userNameFromAcct(const QString &acct); + static QString hostFromAcct(const QString &acct); + +Q_SIGNALS: + void favorite(Choqok::Account *, Choqok::Post *); + +protected Q_SLOTS: + void slotCreatePost(KJob *job); + void slotFavorite(KJob *job); + void slotFetchPost(KJob *job); + void slotReblog(KJob *job); + void slotRemovePost(KJob *job); + void slotUpdateTimeline(KJob *job); + +protected: + static const QString homeTimeline; + static const QString publicTimeline; + + QString authorizationMetaData(MastodonAccount *account) const; + + QString lastTimelineId(Choqok::Account *theAccount, const QString &timeline) const; + + Choqok::Post *readPost(const QVariantMap &var, Choqok::Post *post); + + QList readTimeline(const QByteArray &buffer); + + void setLastTimelineId(Choqok::Account *theAccount, const QString &timeline, + const QString &id); + void setTimelinesInfo(); + + QMap m_accountJobs; + QMap m_createPostJobs; + QMap m_favoriteJobs; + QMap m_removePostJobs; + QMap m_shareJobs; + QMap m_timelinesInfos; + QHash > m_timelinesLatestIds; + QHash m_timelinesPaths; + QMap m_timelinesRequests; + +private: + class Private; + Private *const d; + +}; + +#endif // MASTODONMICROBLOG_H diff --git a/microblogs/mastodon/mastodonoauth.cpp b/microblogs/mastodon/mastodonoauth.cpp new file mode 100644 index 00000000..007b9ae6 --- /dev/null +++ b/microblogs/mastodon/mastodonoauth.cpp @@ -0,0 +1,65 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ + +*/ + +#include "mastodonoauth.h" + +#include +#include + +#include + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodonoauthreplyhandler.h" + +MastodonOAuth::MastodonOAuth(MastodonAccount *account) + : QOAuth2AuthorizationCodeFlow(account), + m_replyHandler(0), m_networkAccessManager(0) +{ + qCDebug(CHOQOK); + + m_replyHandler = new MastodonOAuthReplyHandler(this); + setReplyHandler(m_replyHandler); + + m_networkAccessManager = new KIO::AccessManager(this); + setNetworkAccessManager(m_networkAccessManager); + + setClientIdentifier(account->consumerKey()); + setClientIdentifierSharedKey(account->consumerSecret()); + + setScope(QLatin1String("read write follow")); + + setAccessTokenUrl(QUrl(account->host() + QLatin1String("/oauth/token"))); + setAuthorizationUrl(QUrl(account->host() + QLatin1String("/oauth/authorize"))); +} + +MastodonOAuth::~MastodonOAuth() +{ + m_replyHandler->deleteLater(); + m_networkAccessManager->deleteLater(); +} + +void MastodonOAuth::getToken(const QString &code) +{ + requestAccessToken(code); +} diff --git a/microblogs/mastodon/mastodonoauth.h b/microblogs/mastodon/mastodonoauth.h new file mode 100644 index 00000000..20bee770 --- /dev/null +++ b/microblogs/mastodon/mastodonoauth.h @@ -0,0 +1,55 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ + +*/ + +#ifndef MASTODONOAUTH_H +#define MASTODONOAUTH_H + +#include +#include + +class MastodonAccount; + +namespace KIO { +class AccessManager; +} + +class MastodonOAuthReplyHandler; + +class MastodonOAuth : public QOAuth2AuthorizationCodeFlow +{ + Q_OBJECT +public: + explicit MastodonOAuth(MastodonAccount *account); + ~MastodonOAuth(); + + QByteArray authorizationHeader(const QUrl &requestUrl, QNetworkAccessManager::Operation method, + const QVariantMap ¶ms = QVariantMap()); + + void getToken(const QString &code); + +private: + MastodonOAuthReplyHandler *m_replyHandler; + KIO::AccessManager *m_networkAccessManager; +}; + +#endif // MASTODONOAUTH_H diff --git a/microblogs/mastodon/mastodonoauthreplyhandler.cpp b/microblogs/mastodon/mastodonoauthreplyhandler.cpp new file mode 100644 index 00000000..9d14e574 --- /dev/null +++ b/microblogs/mastodon/mastodonoauthreplyhandler.cpp @@ -0,0 +1,38 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ + +*/ + +#include "mastodonoauthreplyhandler.h" + +MastodonOAuthReplyHandler::MastodonOAuthReplyHandler(QObject *parent) + : QOAuthOobReplyHandler(parent) +{ +} + +MastodonOAuthReplyHandler::~MastodonOAuthReplyHandler() +{ +} + +QString MastodonOAuthReplyHandler::callback() const +{ + return QLatin1String("urn:ietf:wg:oauth:2.0:oob"); +} diff --git a/microblogs/mastodon/mastodonoauthreplyhandler.h b/microblogs/mastodon/mastodonoauthreplyhandler.h new file mode 100644 index 00000000..e7bf642d --- /dev/null +++ b/microblogs/mastodon/mastodonoauthreplyhandler.h @@ -0,0 +1,39 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ + +*/ + +#ifndef MASTODONOAUTHREPLYHANDLER_H +#define MASTODONOAUTHREPLYHANDLER_H + +#include + +class MastodonOAuthReplyHandler : public QOAuthOobReplyHandler +{ + Q_OBJECT +public: + explicit MastodonOAuthReplyHandler(QObject *parent = nullptr); + ~MastodonOAuthReplyHandler(); + + QString callback() const override; +}; + +#endif // MASTODONOAUTHREPLYHANDLER_H diff --git a/microblogs/mastodon/mastodonpost.cpp b/microblogs/mastodon/mastodonpost.cpp new file mode 100644 index 00000000..5fb34aa9 --- /dev/null +++ b/microblogs/mastodon/mastodonpost.cpp @@ -0,0 +1,31 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodonpost.h" + +MastodonPost::MastodonPost() : Post() +{ +} + +MastodonPost::~MastodonPost() +{ +} diff --git a/microblogs/mastodon/mastodonpost.h b/microblogs/mastodon/mastodonpost.h new file mode 100644 index 00000000..a5d5fffe --- /dev/null +++ b/microblogs/mastodon/mastodonpost.h @@ -0,0 +1,38 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#ifndef MASTODONPOST_H +#define MASTODONPOST_H + +#include + +#include "choqoktypes.h" + +class MastodonPost : public Choqok::Post +{ +public: + explicit MastodonPost(); + ~MastodonPost(); + +}; + +#endif //MASTODONPOST_H diff --git a/microblogs/mastodon/mastodonpostwidget.cpp b/microblogs/mastodon/mastodonpostwidget.cpp new file mode 100644 index 00000000..ad44e189 --- /dev/null +++ b/microblogs/mastodon/mastodonpostwidget.cpp @@ -0,0 +1,138 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodonpostwidget.h" + +#include +#include +#include + +#include + +#include "mediamanager.h" +#include "textbrowser.h" + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodonmicroblog.h" +#include "mastodonpost.h" + +const QIcon MastodonPostWidget::unFavIcon(Choqok::MediaManager::convertToGrayScale(QIcon::fromTheme(QLatin1String("rating")).pixmap(16))); + +class MastodonPostWidget::Private +{ +public: + QPushButton *btnFavorite; +}; + +MastodonPostWidget::MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, + QWidget *parent): + PostWidget(account, post, parent), d(new Private) +{ +} + +MastodonPostWidget::~MastodonPostWidget() +{ + delete d; +} + +QString MastodonPostWidget::generateSign() +{ + QString ss; + + MastodonPost *post = dynamic_cast(currentPost()); + MastodonAccount *account = qobject_cast(currentAccount()); + MastodonMicroBlog *microblog = qobject_cast(account->microblog()); + if (post) { + if (post->author.userName != account->username()) { + ss += QLatin1String("profileUrl(account, post->author).toDisplayString() + + QLatin1String("\" title=\"") + post->author.realName + QLatin1String("\">") + + post->author.userName + QLatin1String(" - "); + } + + ss += QLatin1String("postUrl(account, post->author.userName, + post->postId) + QLatin1String("\" title=\"") + + post->creationDateTime.toString(Qt::DefaultLocaleLongDate) + + QLatin1String("\">%1"); + + if (!post->source.isEmpty()) { + ss += QLatin1String(" - ") + post->source; + } + + if (!post->repeatedFromUsername.isEmpty()) { + ss += QLatin1String(" - "); + ss += i18n("Boosted by: ") + microblog->userNameFromAcct(post->repeatedFromUsername); + } + } else { + qCDebug(CHOQOK) << "post is not a MastodonPost!"; + } + + return ss; +} + +void MastodonPostWidget::initUi() +{ + Choqok::UI::PostWidget::initUi(); + + if (isResendAvailable()) { + buttons().value(QLatin1String("btnResend"))->setToolTip(i18nc("@info:tooltip", "Boost")); + } + + d->btnFavorite = addButton(QLatin1String("btnFavorite"), i18nc("@info:tooltip", "Favourite"), QLatin1String("rating")); + d->btnFavorite->setCheckable(true); + connect(d->btnFavorite, SIGNAL(clicked(bool)), this, SLOT(toggleFavorite())); + updateFavStat(); +} + +void MastodonPostWidget::slotResendPost() +{ + qCDebug(CHOQOK); + setReadWithSignal(); + MastodonMicroBlog *microBlog = qobject_cast(currentAccount()->microblog()); + microBlog->toggleReblog(currentAccount(), currentPost()); +} + +void MastodonPostWidget::toggleFavorite() +{ + qCDebug(CHOQOK); + setReadWithSignal(); + MastodonMicroBlog *microBlog = qobject_cast(currentAccount()->microblog()); + connect(microBlog, SIGNAL(favorite(Choqok::Account*,Choqok::Post*)), + this, SLOT(slotToggleFavorite(Choqok::Account*,Choqok::Post*))); + microBlog->toggleFavorite(currentAccount(), currentPost()); +} + +void MastodonPostWidget::slotToggleFavorite(Choqok::Account *, Choqok::Post *) +{ + qCDebug(CHOQOK); + updateFavStat(); +} + +void MastodonPostWidget::updateFavStat() +{ + d->btnFavorite->setChecked(currentPost()->isFavorited); + if (currentPost()->isFavorited) { + d->btnFavorite->setIcon(QIcon::fromTheme(QLatin1String("rating"))); + } else { + d->btnFavorite->setIcon(unFavIcon); + } +} diff --git a/microblogs/mastodon/mastodonpostwidget.h b/microblogs/mastodon/mastodonpostwidget.h new file mode 100644 index 00000000..3c3753ff --- /dev/null +++ b/microblogs/mastodon/mastodonpostwidget.h @@ -0,0 +1,56 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#ifndef MASTODONPOSTWIDGET_H +#define MASTODONPOSTWIDGET_H + +#include "postwidget.h" + +class MastodonPostWidget : public Choqok::UI::PostWidget +{ + Q_OBJECT +public: + explicit MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent = 0); + virtual ~MastodonPostWidget(); + + virtual QString generateSign() override; + + virtual void initUi() override; + +protected Q_SLOTS: + virtual void slotResendPost() override; + + void slotToggleFavorite(Choqok::Account *, Choqok::Post *); + + void toggleFavorite(); + +protected: + static const QIcon unFavIcon; + +private: + void updateFavStat(); + + class Private; + Private *const d; +}; + +#endif // MASTODONPOSTWIDGET_H