diff --git a/README b/README index 3a3c4786..b7506c14 100644 --- a/README +++ b/README @@ -1,32 +1,32 @@ Choqok KDE Micro-Blogging client, -Currently supports Pump.io, GNU Social, Friendica and Twitter.com +Currently supports Pump.io, GNU Social, Friendica, Mastodon and Twitter. Authors: Mehrdad Momeny Andrey Esin Andrea Scarpino License: GNU GPL v2 or v3 or Later Requirements to build: CMake 2.8.12 Qt 5.9 KDE Frameworks libraries 5.6 QCA2-Qt5 library How To Build The Project -=-=-=-=-=-=-=-=-=-=-=-=-= $ cd choqok-src-root-dir [It's choqok-VERSION] $ mkdir build $ cd build $ cmake -DKDE_INSTALL_USE_QT_SYS_PATHS=ON .. $ make $ sudo make install OR su -c 'make install' to uninstall the project: $ make uninstall or su -c 'make uninstall' Feel free to email us. diff --git a/choqok/mainwindow.cpp b/choqok/mainwindow.cpp index 7795e1a0..48909300 100644 --- a/choqok/mainwindow.cpp +++ b/choqok/mainwindow.cpp @@ -1,677 +1,663 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2008-2012 Mehrdad Momeny 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 "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "accountmanager.h" #include "choqokappearancesettings.h" #include "choqokapplication.h" #include "choqokbehaviorsettings.h" #include "choqokdebug.h" #include "choqoktools.h" #include "choqokuiglobal.h" #include "mediamanager.h" #include "microblogwidget.h" #include "pluginmanager.h" #include "postwidget.h" #include "quickpost.h" #include "systrayicon.h" #include "uploadmediadialog.h" -const char *mainButtonStyleSheet = "QPushButton{\ -background-color: qlineargradient(spread:reflect, x1:0.449382, y1:0, x2:0.448, y2:1, stop:0.15 rgba(255, 255, 255, 100), stop:1 rgba(61, 158, 0, 255));\ - border: none;\ - border-radius: 4px;\ - width: 70px;\ - height: 20px;\ - }\ - QPushButton:hover{\ - border: 2px solid rgba(170,170,255,180);\ - }\ - QPushButton:pressed{\ - background-color: qlineargradient(spread:reflect, x1:0.449382, y1:0, x2:0.448, y2:1, stop:0.3 rgba(255, 255, 255, 100), stop:1 rgba(61, 158, 0, 255));\ - }"; - MainWindow::MainWindow(ChoqokApplication *application) : Choqok::UI::MainWindow(), sysIcon(nullptr), quickWidget(nullptr), s_settingsDialog(nullptr), m_splash(nullptr), choqokMainButton(nullptr), app(application), microblogCounter(0), choqokMainButtonVisible(false) { qCDebug(CHOQOK); setAttribute(Qt::WA_DeleteOnClose, false); setAttribute(Qt::WA_QuitOnClose, false); timelineTimer = new QTimer(this); setWindowTitle(i18n("Choqok")); connect(mainWidget, &QTabWidget::currentChanged, this, &MainWindow::slotCurrentBlogChanged); setCentralWidget(mainWidget); setupActions(); updateSysTray(); statusBar()->show(); setupGUI(); if (Choqok::BehaviorSettings::updateInterval() > 0) { mPrevUpdateInterval = Choqok::BehaviorSettings::updateInterval(); } else { mPrevUpdateInterval = 10; } connect(timelineTimer, &QTimer::timeout, this, &MainWindow::updateTimelines); connect(this, &MainWindow::markAllAsRead, this, &MainWindow::slotMarkAllAsRead); connect(Choqok::AccountManager::self(), SIGNAL(accountAdded(Choqok::Account*)), this, SLOT(addBlog(Choqok::Account*))); connect(Choqok::AccountManager::self(), &Choqok::AccountManager::accountRemoved, this, &MainWindow::removeBlog); connect(Choqok::AccountManager::self(), &Choqok::AccountManager::allAccountsLoaded, this, &MainWindow::loadAllAccounts); connect(Choqok::PluginManager::self(), &Choqok::PluginManager::pluginLoaded, this, &MainWindow::newPluginAvailable); QTimer::singleShot(0, Choqok::PluginManager::self(), &Choqok::PluginManager::loadAllPlugins); // Choqok::AccountManager::self()->loadAllAccounts(); QTimer::singleShot(0, Choqok::AccountManager::self(), &Choqok::AccountManager::loadAllAccounts); connect(this, &MainWindow::updateTimelines, this, &MainWindow::slotUpdateTimelines); QPoint pos = Choqok::BehaviorSettings::position(); if (pos.x() != -1 && pos.y() != -1) { move(pos); } actionCollection()->action(QLatin1String("choqok_hide_menubar"))->setChecked(menuBar()->isHidden()); } MainWindow::~MainWindow() { qCDebug(CHOQOK); } void MainWindow::loadAllAccounts() { qCDebug(CHOQOK); if (Choqok::BehaviorSettings::showSplashScreen()) { const QPixmap splashpix(QStandardPaths::locate(QStandardPaths::DataLocation, QLatin1String("images/splash_screen.png"))); if (splashpix.isNull()) { qCCritical(CHOQOK) << "Splash screen pixmap is NULL!"; } else { m_splash = new QSplashScreen(splashpix, Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); m_splash->show(); } } settingsChanged(); QList accList = Choqok::AccountManager::self()->accounts(); const int count = microblogCounter = accList.count(); if (count > 0) { for (Choqok::Account *ac: accList) { connect(ac, &Choqok::Account::status, this, &MainWindow::updateBlog); addBlog(ac, true); } qCDebug(CHOQOK) << "All accounts loaded."; if (Choqok::BehaviorSettings::updateInterval() > 0) { QTimer::singleShot(500, this, &MainWindow::updateTimelines); } } else { if (m_splash) { m_splash->finish(this); delete m_splash; m_splash = nullptr; } } ChoqokApplication::setStartingUp(false); createQuickPostDialog(); } void MainWindow::newPluginAvailable(Choqok::Plugin *plugin) { qCDebug(CHOQOK); guiFactory()->addClient(plugin); } void MainWindow::nextTab(int delta, Qt::Orientation orientation) { if (!isVisible()) { return; } QTabWidget *widget = nullptr; switch (orientation) { case Qt::Vertical: widget = mainWidget; break; case Qt::Horizontal: ///Commented for now! // Choqok::MicroBlogWidget * t = qobject_cast( mainWidget->widget( mainWidget->currentIndex() )); // if(t) // widget = t->tabs; // else return; break; } if (!widget) { return; } int count = widget->count(); int index = widget->currentIndex(); int page; if (delta > 0) { page = index > 0 ? index - 1 : count - 1; } else { page = index < count - 1 ? index + 1 : 0; } widget->setCurrentIndex(page); } void MainWindow::setupActions() { actQuit = KStandardAction::quit(this, SLOT(slotQuit()), actionCollection()); prefs = KStandardAction::preferences(this, SLOT(slotConfigChoqok()), actionCollection()); actUpdate = new QAction(QIcon::fromTheme(QLatin1String("view-refresh")), i18n("Update Timelines"), this); actionCollection()->addAction(QLatin1String("update_timeline"), actUpdate); actionCollection()->setDefaultShortcut(actUpdate, QKeySequence(Qt::Key_F5)); KGlobalAccel::setGlobalShortcut(actUpdate, QKeySequence(Qt::CTRL | Qt::META | Qt::Key_F5)); connect(actUpdate, &QAction::triggered, this, &MainWindow::updateTimelines); newTwit = new QAction(QIcon::fromTheme(QLatin1String("document-new")), i18n("Quick Post"), this); actionCollection()->addAction(QLatin1String("choqok_new_post"), newTwit); actionCollection()->setDefaultShortcut(newTwit, QKeySequence(Qt::CTRL | Qt::Key_T)); KGlobalAccel::setGlobalShortcut(newTwit, QKeySequence(Qt::CTRL | Qt::META | Qt::Key_T)); connect(newTwit, &QAction::triggered, this, &MainWindow::triggerQuickPost); QAction *markRead = new QAction(QIcon::fromTheme(QLatin1String("mail-mark-read")), i18n("Mark All As Read"), this); actionCollection()->addAction(QLatin1String("choqok_mark_as_read"), markRead); actionCollection()->setDefaultShortcut(markRead, QKeySequence(Qt::CTRL | Qt::Key_R)); connect(markRead, &QAction::triggered, this, &MainWindow::markAllAsRead); showMain = new QAction(this); actionCollection()->addAction(QLatin1String("toggle_mainwin"), showMain); KGlobalAccel::setGlobalShortcut(showMain, QKeySequence(Qt::CTRL | Qt::META | Qt::Key_C)); if (this->isVisible()) { showMain->setText(i18nc("@action", "Minimize")); } else { showMain->setText(i18nc("@action", "Restore")); } connect(showMain, &QAction::triggered, this, &MainWindow::toggleMainWindow); QAction *act = KStandardAction::configureNotifications(this, SLOT(slotConfNotifications()), actionCollection()); actionCollection()->addAction(QLatin1String("settings_notifications"), act); enableUpdates = new QAction(i18n("Enable Update Timer"), this); enableUpdates->setCheckable(true); actionCollection()->addAction(QLatin1String("choqok_enable_updates"), enableUpdates); actionCollection()->setDefaultShortcut(enableUpdates, QKeySequence(Qt::CTRL | Qt::Key_U)); connect(enableUpdates, &QAction::toggled, this, &MainWindow::setTimeLineUpdatesEnabled); QAction *enableNotify = new QAction(i18n("Enable Notifications"), this); enableNotify->setCheckable(true); actionCollection()->addAction(QLatin1String("choqok_enable_notify"), enableNotify); actionCollection()->setDefaultShortcut(enableNotify, QKeySequence(Qt::CTRL | Qt::Key_N)); connect(enableNotify, &QAction::toggled, this, &MainWindow::setNotificationsEnabled); QAction *hideMenuBar = new QAction(i18n("Hide Menubar"), this); hideMenuBar->setCheckable(true); actionCollection()->addAction(QLatin1String("choqok_hide_menubar"), hideMenuBar); actionCollection()->setDefaultShortcut(hideMenuBar, QKeySequence(Qt::ControlModifier | Qt::Key_M)); connect(hideMenuBar, &QAction::toggled, menuBar(), &QMenuBar::setHidden); connect(hideMenuBar, &QAction::toggled, this, &MainWindow::slotShowSpecialMenu); QAction *clearAvatarCache = new QAction(QIcon::fromTheme(QLatin1String("edit-clear")), i18n("Clear Avatar Cache"), this); actionCollection()->addAction(QLatin1String("choqok_clear_avatar_cache"), clearAvatarCache); QString tip = i18n("You have to restart Choqok to load avatars again"); clearAvatarCache->setToolTip(tip); clearAvatarCache->setStatusTip(tip); connect(clearAvatarCache, &QAction::triggered, Choqok::MediaManager::self(), &Choqok::MediaManager::clearImageCache); QAction *uploadMedium = new QAction(QIcon::fromTheme(QLatin1String("arrow-up")), i18n("Upload Medium..."), this); actionCollection()->addAction(QLatin1String("choqok_upload_medium"), uploadMedium); connect(uploadMedium, &QAction::triggered, this, &MainWindow::slotUploadMedium); } void MainWindow::slotConfNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::createQuickPostDialog() { quickWidget = new Choqok::UI::QuickPost(this); Choqok::UI::Global::setQuickPostWidget(quickWidget); quickWidget->setAttribute(Qt::WA_DeleteOnClose, false); if (sysIcon) { connect(quickWidget, &Choqok::UI::QuickPost::newPostSubmitted, sysIcon, &SysTrayIcon::slotJobDone); } Q_EMIT quickPostCreated(); } void MainWindow::triggerQuickPost() { if (Choqok::AccountManager::self()->accounts().isEmpty()) { KMessageBox::error(this, i18n("No account created. You have to create an account before being able to make posts.")); return; } if (!quickWidget) { createQuickPostDialog(); } if (quickWidget->isVisible()) { quickWidget->hide(); } else { quickWidget->show(); } } void MainWindow::slotConfigChoqok() { if (!s_settingsDialog) { s_settingsDialog = new KSettings::Dialog(this); } s_settingsDialog->show(); connect(Choqok::BehaviorSettings::self(), &Choqok::BehaviorSettings::configChanged, this, &MainWindow::slotBehaviorConfigChanged); connect(Choqok::AppearanceSettings::self(), &Choqok::AppearanceSettings::configChanged, this, &MainWindow::slotAppearanceConfigChanged); } void MainWindow::settingsChanged() { qCDebug(CHOQOK); if (Choqok::AccountManager::self()->accounts().count() < 1) { if (KMessageBox::questionYesNo(this, i18n("In order to use Choqok you need \ an account at one of the supported micro-blogging services.\n\ Would you like to add your account now?")) == KMessageBox::Yes) { slotConfigChoqok(); } } slotAppearanceConfigChanged(); slotBehaviorConfigChanged(); } void MainWindow::slotAppearanceConfigChanged() { if (Choqok::AppearanceSettings::isCustomUi()) { Choqok::UI::PostWidget::setStyle(Choqok::AppearanceSettings::unreadForeColor() , Choqok::AppearanceSettings::unreadBackColor(), Choqok::AppearanceSettings::readForeColor() , Choqok::AppearanceSettings::readBackColor() , Choqok::AppearanceSettings::ownForeColor() , Choqok::AppearanceSettings::ownBackColor(), Choqok::AppearanceSettings::font()); } else { QPalette p = window()->palette(); Choqok::UI::PostWidget::setStyle(p.color(QPalette::WindowText) , p.color(QPalette::Window).lighter() , p.color(QPalette::WindowText) , p.color(QPalette::Window) , p.color(QPalette::WindowText) , p.color(QPalette::Window), font()); } for (int i = 0; i < mainWidget->count(); ++i) { qobject_cast(mainWidget->widget(i))->settingsChanged(); } } void MainWindow::updateSysTray() { if (Choqok::BehaviorSettings::enableSysTray()) { if (!sysIcon) { sysIcon = new SysTrayIcon(this); // sysIcon->show(); ///SysTray Actions: sysIcon->contextMenu()->addAction(newTwit); // sysIcon->contextMenu()->addAction( uploadMedium ); sysIcon->contextMenu()->addAction(actUpdate); sysIcon->contextMenu()->addSeparator(); connect(enableUpdates, &QAction::toggled, sysIcon, &SysTrayIcon::setTimeLineUpdatesEnabled); sysIcon->contextMenu()->addAction(enableUpdates); sysIcon->setTimeLineUpdatesEnabled(enableUpdates->isChecked()); // sysIcon->contextMenu()->addAction( enableNotify ); sysIcon->contextMenu()->addAction(prefs); sysIcon->contextMenu()->addSeparator(); sysIcon->contextMenu()->addAction(showMain); sysIcon->contextMenu()->addAction(actQuit); // connect( sysIcon, SIGNAL(quitSelected()), this, SLOT(slotQuit()) ); connect(sysIcon, &SysTrayIcon::scrollRequested, this, &MainWindow::nextTab); } } else { if (sysIcon) { if (isHidden()) { show(); } delete sysIcon; sysIcon = nullptr; } } } void MainWindow::slotBehaviorConfigChanged() { if (Choqok::BehaviorSettings::notifyEnabled()) { actionCollection()->action(QLatin1String("choqok_enable_notify"))->setChecked(true); } else { actionCollection()->action(QLatin1String("choqok_enable_notify"))->setChecked(false); } if (Choqok::BehaviorSettings::updateInterval() > 0) { timelineTimer->setInterval(Choqok::BehaviorSettings::updateInterval() * 60000); timelineTimer->start(); actionCollection()->action(QLatin1String("choqok_enable_updates"))->setChecked(true); } else { timelineTimer->stop(); actionCollection()->action(QLatin1String("choqok_enable_updates"))->setChecked(false); } updateSysTray(); } void MainWindow::slotQuit() { qCDebug(CHOQOK); Choqok::BehaviorSettings::setPosition(pos()); timelineTimer->stop(); Choqok::BehaviorSettings::self()->save(); app->quitChoqok(); } void MainWindow::disableApp() { qCDebug(CHOQOK); timelineTimer->stop(); // qCDebug(CHOQOK)<<"timelineTimer stoped"; actionCollection()->action(QLatin1String("update_timeline"))->setEnabled(false); actionCollection()->action(QLatin1String("choqok_new_post"))->setEnabled(false); // actionCollection()->action( "choqok_search" )->setEnabled( false ); actionCollection()->action(QLatin1String("choqok_mark_as_read"))->setEnabled(false); // actionCollection()->action( "choqok_now_listening" )->setEnabled( false ); } void MainWindow::enableApp() { qCDebug(CHOQOK); if (Choqok::BehaviorSettings::updateInterval() > 0) { timelineTimer->start(); // qCDebug(CHOQOK)<<"timelineTimer started"; } actionCollection()->action(QLatin1String("update_timeline"))->setEnabled(true); actionCollection()->action(QLatin1String("choqok_new_post"))->setEnabled(true); // actionCollection()->action( "choqok_search" )->setEnabled( true ); actionCollection()->action(QLatin1String("choqok_mark_as_read"))->setEnabled(true); // actionCollection()->action( "choqok_now_listening" )->setEnabled( true ); } void MainWindow::addBlog(Choqok::Account *account, bool isStartup) { qCDebug(CHOQOK) << "Adding new Blog, Alias:" << account->alias() << "Blog:" << account->microblog()->serviceName(); if (!account->isEnabled()) { oneMicroblogLoaded(); return; } Choqok::UI::MicroBlogWidget *widget = account->microblog()->createMicroBlogWidget(account, this); connect(widget, &Choqok::UI::MicroBlogWidget::loaded, this, &MainWindow::oneMicroblogLoaded); connect(widget, &Choqok::UI::MicroBlogWidget::updateUnreadCount, this, &MainWindow::slotUpdateUnreadCount); widget->initUi(); connect(widget, &Choqok::UI::MicroBlogWidget::showMe, this, &MainWindow::showBlog); connect(this, &MainWindow::updateTimelines, widget, &Choqok::UI::MicroBlogWidget::updateTimelines); connect(this, &MainWindow::markAllAsRead, widget, &Choqok::UI::MicroBlogWidget::markAllAsRead); connect(this, &MainWindow::removeOldPosts, widget, &Choqok::UI::MicroBlogWidget::removeOldPosts); mainWidget->addTab(widget, QIcon::fromTheme(account->microblog()->pluginIcon()), account->alias()); if (!isStartup) { QTimer::singleShot(1500, widget, &Choqok::UI::MicroBlogWidget::updateTimelines); } enableApp(); updateTabbarHiddenState(); } void MainWindow::updateBlog(Choqok::Account *account, bool enabled) { if (!enabled) { removeBlog(account->alias()); } else { addBlog(account); } } void MainWindow::removeBlog(const QString &alias) { qCDebug(CHOQOK); for (int i = 0; i < mainWidget->count(); ++i) { Choqok::UI::MicroBlogWidget *tmp = qobject_cast(mainWidget->widget(i)); if (tmp->currentAccount()->alias() == alias) { mainWidget->removeTab(i); if (mainWidget->count() < 1) { disableApp(); } tmp->deleteLater(); updateTabbarHiddenState(); return; } } } void MainWindow::updateTabbarHiddenState() { if (mainWidget->count() <= 1 && !choqokMainButtonVisible) { mainWidget->tabBar()->hide(); } else { mainWidget->tabBar()->show(); } } void MainWindow::slotUpdateUnreadCount(int change, int sum) { qCDebug(CHOQOK) << "Change:" << change << "Sum:" << sum; Choqok::UI::MicroBlogWidget *wd = qobject_cast(sender()); if (sysIcon) { sysIcon->updateUnreadCount(change); } int accountsSum = 0; for (int i = 0; i < mainWidget->tabBar()->count(); ++i) { Choqok::UI::MicroBlogWidget *tab = qobject_cast(mainWidget->widget(i)); accountsSum += tab->unreadCount(); if (wd == tab) { if (sum > 0) { mainWidget->setTabText(i, wd->currentAccount()->alias() + QStringLiteral(" (%1)").arg(sum)); } else { mainWidget->setTabText(i, wd->currentAccount()->alias()); } } } if (accountsSum > 0) { setWindowTitle(i18n("Choqok (%1)", accountsSum)); } else { setWindowTitle(i18n("Choqok")); } } void MainWindow::showBlog() { mainWidget->setCurrentWidget(qobject_cast(sender())); if (!this->isVisible()) { this->show(); } this->raise(); } void MainWindow::setTimeLineUpdatesEnabled(bool isEnabled) { qCDebug(CHOQOK); if (isEnabled) { if (mPrevUpdateInterval > 0) { Choqok::BehaviorSettings::setUpdateInterval(mPrevUpdateInterval); } Q_EMIT updateTimelines(); timelineTimer->start(Choqok::BehaviorSettings::updateInterval() * 60000); // qCDebug(CHOQOK)<<"timelineTimer started"; } else { mPrevUpdateInterval = Choqok::BehaviorSettings::updateInterval(); timelineTimer->stop(); // qCDebug(CHOQOK)<<"timelineTimer stoped"; Choqok::BehaviorSettings::setUpdateInterval(0); } } void MainWindow::setNotificationsEnabled(bool isEnabled) { qCDebug(CHOQOK); if (isEnabled) { Choqok::BehaviorSettings::setNotifyEnabled(true); } else { Choqok::BehaviorSettings::setNotifyEnabled(false); } } void MainWindow::toggleMainWindow() { if (this->isVisible()) { hide(); } else { show(); } } void MainWindow::hideEvent(QHideEvent *event) { Choqok::UI::MainWindow::hideEvent(event); showMain->setText(i18n("Restore")); } void MainWindow::showEvent(QShowEvent *event) { Choqok::UI::MainWindow::showEvent(event); showMain->setText(i18n("Minimize")); } void MainWindow::slotMarkAllAsRead() { setWindowTitle(i18n("Choqok")); if (sysIcon) { sysIcon->resetUnreadCount(); } for (int i = 0; i < mainWidget->count(); ++i) { Choqok::UI::MicroBlogWidget *wd = qobject_cast(mainWidget->widget(i)); mainWidget->setTabText(i, wd->currentAccount()->alias()); } } void MainWindow::slotCurrentBlogChanged(int) { Choqok::UI::MicroBlogWidget *wd = qobject_cast(mainWidget->currentWidget()); if (wd) { wd->setFocus(); Q_EMIT currentMicroBlogWidgetChanged(wd); } } void MainWindow::oneMicroblogLoaded() { qCDebug(CHOQOK); --microblogCounter; if (microblogCounter < 1) { //Everything loaded, So splash screen should be deleted! delete m_splash; m_splash = nullptr; if (Choqok::BehaviorSettings::showMainWinOnStart()) { this->show(); } } else { if (m_splash) { m_splash->showMessage(QString()); //Workaround for Qt 4.8 splash bug } } } void MainWindow::slotUpdateTimelines() { if (Choqok::AccountManager::self()->accounts().count()) { showStatusMessage(i18n("Loading timelines...")); } } void MainWindow::slotUploadMedium() { QPointer dlg = new Choqok::UI::UploadMediaDialog(this); dlg->show(); } void MainWindow::slotShowSpecialMenu(bool show) { if (show) { if (!choqokMainButton) { choqokMainButton = new QPushButton(QIcon::fromTheme(QLatin1String("choqok")), QString(), mainWidget); QMenu *menu = new QMenu(i18n("Choqok"), choqokMainButton); menu->addAction(actionCollection()->action(QLatin1String("choqok_new_post"))); menu->addAction(actionCollection()->action(QLatin1String("update_timeline"))); menu->addAction(actionCollection()->action(QLatin1String("choqok_mark_as_read"))); menu->addAction(actionCollection()->action(QLatin1String("choqok_upload_medium"))); menu->addSeparator(); menu->addAction(actionCollection()->action(QLatin1String("choqok_enable_updates"))); menu->addAction(actionCollection()->action(QLatin1String("choqok_hide_menubar"))); menu->addAction(prefs); menu->addSeparator(); menu->addAction(showMain); menu->addAction(actQuit); choqokMainButton->setMenu(menu); } mainWidget->setCornerWidget(choqokMainButton/*, Qt::TopLeftCorner*/); choqokMainButton->show(); choqokMainButtonVisible = true; } else { choqokMainButtonVisible = false; mainWidget->setCornerWidget(nullptr/*, Qt::TopLeftCorner*/); } updateTabbarHiddenState(); } diff --git a/libchoqok/account.cpp b/libchoqok/account.cpp index 1dc4dc35..debec72c 100644 --- a/libchoqok/account.cpp +++ b/libchoqok/account.cpp @@ -1,192 +1,192 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2008-2012 Mehrdad Momeny 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 "account.h" #include #include "libchoqokdebug.h" #include "microblog.h" #include "passwordmanager.h" namespace Choqok { class Account::Private { public: Private(Choqok::MicroBlog *parent, const QString &mAlias) : alias(mAlias), blog(parent) { configGroup = new KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("Account_%1").arg(alias)); username = configGroup->readEntry("Username", QString()); - priority = configGroup->readEntry("Priority", (uint)0); + priority = configGroup->readEntry("Priority", static_cast(0)); readonly = configGroup->readEntry("ReadOnly", false); showInQuickPost = configGroup->readEntry("ShowInQuickPost", true); enable = configGroup->readEntry("Enable", true); - postCharLimit = configGroup->readEntry("PostCharLimit", 140); + postCharLimit = configGroup->readEntry("PostCharLimit", static_cast(140)); password = PasswordManager::self()->readPassword(alias); } QString username; QString password; QString alias; MicroBlog *blog; KConfigGroup *configGroup; uint priority; bool readonly; bool enable; bool showInQuickPost; uint postCharLimit; }; Account::Account(Choqok::MicroBlog *parent, const QString &alias) : QObject(parent), d(new Private(parent, alias)) { qCDebug(CHOQOK); } Account::~Account() { qCDebug(CHOQOK) << alias(); // writeConfig(); delete d->configGroup; } void Account::writeConfig() { d->configGroup->writeEntry("Alias", d->alias); d->configGroup->writeEntry("Username", d->username); d->configGroup->writeEntry("Priority", d->priority); d->configGroup->writeEntry("ReadOnly", d->readonly); d->configGroup->writeEntry("Enable", d->enable); d->configGroup->writeEntry("ShowInQuickPost", d->showInQuickPost); d->configGroup->writeEntry("MicroBlog", microblog()->pluginName()); d->configGroup->writeEntry("PostCharLimit", d->postCharLimit); if (!password().isEmpty()) { PasswordManager::self()->writePassword(d->alias, password()); } d->configGroup->sync(); Q_EMIT modified(this); } QString Account::username() const { return d->username; } void Account::setUsername(const QString &name) { d->username = name; } QString Account::password() const { return d->password; } void Account::setPassword(const QString &pass) { d->password = pass; } QString Account::alias() const { return d->alias; } void Account::setAlias(const QString &alias) { d->alias = alias; d->configGroup->deleteGroup(); delete d->configGroup; d->configGroup = new KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("Account_%1").arg(d->alias)); writeConfig(); } bool Account::isReadOnly() const { return d->readonly; } void Account::setReadOnly(bool readonly /*= true*/) { d->readonly = readonly; } MicroBlog *Account::microblog() const { return d->blog; } void Account::setPriority(uint priority) { d->priority = priority; // d->configGroup->writeEntry( "Priority", d->priority ); } uint Account::priority() const { return d->priority; } bool Account::isEnabled() const { return d->enable; } void Account::setEnabled(bool enabled) { d->enable = enabled; Q_EMIT status(this, enabled); } uint Account::postCharLimit() const { return d->postCharLimit; } void Account::setPostCharLimit(const uint limit) { d->postCharLimit = limit; } bool Account::showInQuickPost() const { return d->showInQuickPost; } void Account::setShowInQuickPost(bool show) { d->showInQuickPost = show; } KConfigGroup *Account::configGroup() const { return d->configGroup; } QStringList Account::timelineNames() const { return d->blog->timelineNames(); } } diff --git a/libchoqok/microblog.cpp b/libchoqok/microblog.cpp index a91b5ef6..06b6bfed 100644 --- a/libchoqok/microblog.cpp +++ b/libchoqok/microblog.cpp @@ -1,234 +1,228 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2008-2012 Mehrdad Momeny 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 "microblog.h" #include #include #include #include "account.h" #include "accountmanager.h" #include "choqokbehaviorsettings.h" #include "composerwidget.h" #include "libchoqokdebug.h" #include "microblogwidget.h" #include "postwidget.h" #include "timelinewidget.h" namespace Choqok { class MicroBlog::Private { public: QString serviceName; QString homepage; QStringList timelineTypes; QTimer *saveTimelinesTimer; }; MicroBlog::MicroBlog(const QString &componentName, QObject *parent) : Plugin(componentName, parent), d(new Private) { qCDebug(CHOQOK); d->saveTimelinesTimer = new QTimer(this); d->saveTimelinesTimer->setInterval(BehaviorSettings::notifyInterval() * 60000); connect(d->saveTimelinesTimer, &QTimer::timeout, this, &MicroBlog::saveTimelines); connect(BehaviorSettings::self(), &BehaviorSettings::configChanged, this, &MicroBlog::slotConfigChanged); d->saveTimelinesTimer->start(); } MicroBlog::~MicroBlog() { qCDebug(CHOQOK); delete d; } QMenu *MicroBlog::createActionsMenu(Account *, QWidget *parent) { return new QMenu(parent); } QString MicroBlog::serviceName() const { return d->serviceName; } QString MicroBlog::homepageUrl() const { return d->homepage; } QString MicroBlog::errorString(ErrorType type) { switch (type) { case ServerError: return i18n("The server returned an error"); - break; case CommunicationError: return i18n("Error on communication with server"); - break; case ParsingError: return i18n("Error on parsing results"); - break; case AuthenticationError: return i18n("Authentication error"); - break; case NotSupportedError: return i18n("The server does not support this feature"); - break; case OtherError: return i18n("Unknown error"); - break; - }; + } return QString(); } void MicroBlog::setServiceName(const QString &serviceName) { d->serviceName = serviceName; } void MicroBlog::setServiceHomepageUrl(const QString &homepage) { d->homepage = homepage; } QStringList MicroBlog::timelineNames() const { return d->timelineTypes; } void MicroBlog::setTimelineNames(const QStringList &types) { d->timelineTypes = types; } void MicroBlog::addTimelineName(const QString &name) { d->timelineTypes << name; } bool MicroBlog::isValidTimeline(const QString &timeline) { return d->timelineTypes.contains(timeline); } void MicroBlog::slotConfigChanged() { d->saveTimelinesTimer->setInterval(BehaviorSettings::notifyInterval() * 60000); } /// UI Objects: Account *MicroBlog::createNewAccount(const QString &alias) { Choqok::Account *acc = Choqok::AccountManager::self()->findAccount(alias); if (!acc) { return new Choqok::Account(this, alias); } else { return nullptr; } } UI::MicroBlogWidget *MicroBlog::createMicroBlogWidget(Account *account, QWidget *parent) { return new UI::MicroBlogWidget(account, parent); } UI::ComposerWidget *MicroBlog::createComposerWidget(Account *account, QWidget *parent) { return new UI::ComposerWidget(account, parent); } UI::TimelineWidget *MicroBlog::createTimelineWidget(Account *account, const QString &timelineName, QWidget *parent) { return new UI::TimelineWidget(account, timelineName, parent); } UI::PostWidget *MicroBlog::createPostWidget(Account *account, Choqok::Post *post, QWidget *parent) { return new UI::PostWidget(account, post, parent); } TimelineInfo *MicroBlog::timelineInfo(const QString &) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; return nullptr; } void MicroBlog::abortAllJobs(Account *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } void MicroBlog::abortCreatePost(Account *, Post *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } void MicroBlog::createPost(Account *, Post *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } void MicroBlog::fetchPost(Account *, Post *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } void MicroBlog::removePost(Account *, Post *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } void MicroBlog::updateTimelines(Account *) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } QList< Post * > MicroBlog::loadTimeline(Account *, const QString &) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; return QList(); } void MicroBlog::saveTimeline(Account *, const QString &, const QList< UI::PostWidget * > &) { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; } QUrl MicroBlog::postUrl(Account *, const QString &, const QString &) const { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; return QUrl(); } QUrl MicroBlog::profileUrl(Account *, const QString &) const { qCWarning(CHOQOK) << "MicroBlog Plugin should implement this!"; return QUrl(); } } 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..6a81fbb7 --- /dev/null +++ b/microblogs/mastodon/CMakeLists.txt @@ -0,0 +1,43 @@ +include_directories( + ${CHOQOK_INCLUDES} +) + +set(choqok_mastodon_SRCS + mastodonaccount.cpp + mastodoncomposerwidget.cpp + mastodondebug.cpp + mastodondmessagedialog.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..46b7d9ef --- /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=https://choqok.kde.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..1f74d953 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..1530d667 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..a65fe448 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..10bbd241 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..4ad30aed 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..2acb87d1 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..36770cf3 --- /dev/null +++ b/microblogs/mastodon/mastodonaccount.cpp @@ -0,0 +1,168 @@ +/* + 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; + uint id; + QString tokenSecret; + QStringList followers; + 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->id = configGroup()->readEntry("Id", uint()); + d->followers = configGroup()->readEntry("Followers", QStringList()); + d->following = configGroup()->readEntry("Following", QStringList()); + d->lists = configGroup()->readEntry("Lists", QVariantList()); + 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); + + parent->fetchFollowers(this, false); + parent->fetchFollowing(this, false); +} + +MastodonAccount::~MastodonAccount() +{ + d->oAuth->deleteLater(); + delete d; +} + +void MastodonAccount::writeConfig() +{ + configGroup()->writeEntry("Host", d->host); + configGroup()->writeEntry("Id", d->id); + configGroup()->writeEntry("ConsumerKey", d->consumerKey); + configGroup()->writeEntry("Followers", d->followers); + configGroup()->writeEntry("Following", d->following); + configGroup()->writeEntry("Lists", d->lists); + + 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; +} + +uint MastodonAccount::id() +{ + return d->id; +} + +void MastodonAccount::setId(const uint id) +{ + d->id = id; +} + +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; +} + +QStringList MastodonAccount::followers() { + return d->followers; +} + +void MastodonAccount::setFollowers(const QStringList &followers) { + d->followers = followers; + writeConfig(); +} + +QStringList MastodonAccount::following() { + return d->following; +} + +void MastodonAccount::setFollowing(const QStringList &following) { + d->following = following; + writeConfig(); +} + +QVariantList MastodonAccount::lists() { + return d->lists; +} + +void MastodonAccount::setLists(const QVariantList &lists) { + d->lists = lists; + writeConfig(); +} diff --git a/microblogs/mastodon/mastodonaccount.h b/microblogs/mastodon/mastodonaccount.h new file mode 100644 index 00000000..7ba4e2b8 --- /dev/null +++ b/microblogs/mastodon/mastodonaccount.h @@ -0,0 +1,74 @@ +/* + 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); + + uint id(); + void setId(const uint id); + + QString consumerKey(); + void setConsumerKey(const QString &consumerKey); + + QString consumerSecret(); + void setConsumerSecret(const QString &consumerSecret); + + QString tokenSecret(); + void setTokenSecret(const QString &tokenSecret); + + MastodonOAuth *oAuth(); + + QStringList followers(); + void setFollowers(const QStringList &followers); + + QStringList following(); + void setFollowing(const QStringList &following); + + QVariantList lists(); + void setLists(const QVariantList &lists); + +private: + class Private; + Private *d; + +}; + +#endif // MASTODONACCOUNT_H diff --git a/microblogs/mastodon/mastodoncomposerwidget.cpp b/microblogs/mastodon/mastodoncomposerwidget.cpp new file mode 100644 index 00000000..32e525c5 --- /dev/null +++ b/microblogs/mastodon/mastodoncomposerwidget.cpp @@ -0,0 +1,169 @@ +/* + 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 "mastodoncomposerwidget.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "account.h" +#include "choqoktextedit.h" +#include "shortenmanager.h" + +#include "mastodondebug.h" +#include "mastodonmicroblog.h" +#include "mastodonpost.h" + +class MastodonComposerWidget::Private +{ +public: + QString mediumToAttach; + QPushButton *btnAttach; + QPointer mediumName; + QPointer btnCancel; + QGridLayout *editorLayout; +}; + +MastodonComposerWidget::MastodonComposerWidget(Choqok::Account *account, QWidget *parent) + : ComposerWidget(account, parent) + , d(new Private) +{ + d->editorLayout = qobject_cast(editorContainer()->layout()); + d->btnAttach = new QPushButton(editorContainer()); + d->btnAttach->setIcon(QIcon::fromTheme(QLatin1String("mail-attachment"))); + d->btnAttach->setToolTip(i18n("Attach a file")); + d->btnAttach->setMaximumWidth(d->btnAttach->height()); + connect(d->btnAttach, &QPushButton::clicked, this, &MastodonComposerWidget::attachMedia); + QVBoxLayout *vLayout = new QVBoxLayout; + vLayout->addWidget(d->btnAttach); + vLayout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Preferred, QSizePolicy::MinimumExpanding)); + d->editorLayout->addItem(vLayout, 0, 1); +} + +MastodonComposerWidget::~MastodonComposerWidget() +{ + delete d; +} + +void MastodonComposerWidget::submitPost(const QString &text) +{ + qCDebug(CHOQOK); + editorContainer()->setEnabled(false); + QString txt = text; + if (currentAccount()->postCharLimit() && + txt.size() > (int) currentAccount()->postCharLimit()) { + txt = Choqok::ShortenManager::self()->parseText(txt); + } + setPostToSubmit(nullptr); + setPostToSubmit(new Choqok::Post); + postToSubmit()->content = txt; + if (!replyToId.isEmpty()) { + postToSubmit()->replyToPostId = replyToId; + } + connect(currentAccount()->microblog(), &Choqok::MicroBlog::postCreated, this, + &MastodonComposerWidget::slotPostSubmited); + connect(currentAccount()->microblog(), &Choqok::MicroBlog::errorPost, this, + &MastodonComposerWidget::slotErrorPost); + btnAbort = new QPushButton(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("Abort"), this); + layout()->addWidget(btnAbort); + connect(btnAbort, &QPushButton::clicked, this, &MastodonComposerWidget::abort); + + MastodonMicroBlog *mBlog = qobject_cast(currentAccount()->microblog()); + if (d->mediumToAttach.isEmpty()) { + if (replyToId.isEmpty()) { + currentAccount()->microblog()->createPost(currentAccount(), postToSubmit()); + } else { + // WTF? It seems we cannot cast postToSubmit to MastodonPost and then I'm copying its attributes + MastodonPost *pumpPost = new MastodonPost(); + pumpPost->content = postToSubmit()->content; + pumpPost->replyToPostId = postToSubmit()->replyToPostId; + setPostToSubmit(pumpPost); + + mBlog->createReply(currentAccount(), pumpPost); + } + } else { + // TODO + // mBlog->createPostWithMedia(currentAccount(), postToSubmit(), d->mediumToAttach); + } +} + +void MastodonComposerWidget::slotPostSubmited(Choqok::Account *theAccount, Choqok::Post *post) +{ + qCDebug(CHOQOK); + if (currentAccount() == theAccount && post == postToSubmit()) { + qCDebug(CHOQOK) << "Accepted"; + disconnect(currentAccount()->microblog(), &Choqok::MicroBlog::postCreated, + this, &MastodonComposerWidget::slotPostSubmited); + disconnect(currentAccount()->microblog(), &Choqok::MicroBlog::errorPost, + this, &MastodonComposerWidget::slotErrorPost); + if (btnAbort) { + btnAbort->deleteLater(); + } + editor()->clear(); + editorCleared(); + editorContainer()->setEnabled(true); + setPostToSubmit(nullptr); + cancelAttach(); + currentAccount()->microblog()->updateTimelines(currentAccount()); + } +} + +void MastodonComposerWidget::attachMedia() +{ + qCDebug(CHOQOK); + d->mediumToAttach = QFileDialog::getOpenFileName(this, i18n("Select Media to Upload"), + QString(), QStringLiteral("Images")); + if (d->mediumToAttach.isEmpty()) { + qCDebug(CHOQOK) << "No file selected"; + return; + } + const QString fileName = QUrl(d->mediumToAttach).fileName(); + if (!d->mediumName) { + d->mediumName = new QLabel(editorContainer()); + d->btnCancel = new QPushButton(editorContainer()); + d->btnCancel->setIcon(QIcon::fromTheme(QLatin1String("list-remove"))); + d->btnCancel->setToolTip(i18n("Discard Attachment")); + d->btnCancel->setMaximumWidth(d->btnCancel->height()); + connect(d->btnCancel, &QPushButton::clicked, this, &MastodonComposerWidget::cancelAttach); + + d->editorLayout->addWidget(d->mediumName, 1, 0); + d->editorLayout->addWidget(d->btnCancel, 1, 1); + } + d->mediumName->setText(i18n("Attaching %1", fileName)); + editor()->setFocus(); +} + +void MastodonComposerWidget::cancelAttach() +{ + qCDebug(CHOQOK); + delete d->mediumName; + d->mediumName = 0; + delete d->btnCancel; + d->btnCancel = 0; + d->mediumToAttach.clear(); +} diff --git a/microblogs/mastodon/mastodoncomposerwidget.h b/microblogs/mastodon/mastodoncomposerwidget.h new file mode 100644 index 00000000..003e7872 --- /dev/null +++ b/microblogs/mastodon/mastodoncomposerwidget.h @@ -0,0 +1,48 @@ +/* + 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 MASTODONCOMPOSERWIDGET_H +#define MASTODONCOMPOSERWIDGET_H + +#include "composerwidget.h" + +class MastodonComposerWidget : public Choqok::UI::ComposerWidget +{ + Q_OBJECT +public: + explicit MastodonComposerWidget(Choqok::Account *account, QWidget *parent = nullptr); + ~MastodonComposerWidget(); + +protected Q_SLOTS: + virtual void submitPost(const QString &text) override; + virtual void slotPostSubmited(Choqok::Account *theAccount, Choqok::Post *post) override; + + void cancelAttach(); + void attachMedia(); + +private: + class Private; + Private *const d; + +}; + +#endif // MASTODONCOMPOSERWIDGET_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/mastodondmessagedialog.cpp b/microblogs/mastodon/mastodondmessagedialog.cpp new file mode 100644 index 00000000..7a6adb74 --- /dev/null +++ b/microblogs/mastodon/mastodondmessagedialog.cpp @@ -0,0 +1,190 @@ +/* + 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 "mastodondmessagedialog.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "choqoktextedit.h" +#include "microblog.h" +#include "notifymanager.h" + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodonmicroblog.h" + +class MastodonDMessageDialog::Private +{ +public: + Private(MastodonAccount *theAccount) + : account(theAccount) + {} + QComboBox *comboFriendsList; + Choqok::UI::TextEdit *editor; + MastodonAccount *account; + Choqok::Post *sentPost; +}; + +MastodonDMessageDialog::MastodonDMessageDialog(MastodonAccount *theAccount, QWidget *parent, + Qt::WindowFlags flags) + : QDialog(parent, flags), d(new Private(theAccount)) +{ + setWindowTitle(i18n("Send Private Message")); + setAttribute(Qt::WA_DeleteOnClose); + setupUi(this); + KConfigGroup grp(KSharedConfig::openConfig(), "Mastodon"); + resize(grp.readEntry("DMessageDialogSize", QSize(300, 200))); + QStringList list = theAccount->followers(); + if (list.isEmpty()) { + reloadFriendslist(); + } else { + list.sort(Qt::CaseInsensitive); + d->comboFriendsList->addItems(list); + } +} + +MastodonDMessageDialog::~MastodonDMessageDialog() +{ + KConfigGroup grp(KSharedConfig::openConfig(), "Mastodon"); + grp.writeEntry("DMessageDialogSize", size()); + grp.sync(); + delete d; +} + +void MastodonDMessageDialog::setupUi(QWidget *mainWidget) +{ + QLabel *lblTo = new QLabel(i18nc("Send message to", "To:"), this); + d->comboFriendsList = new QComboBox(this); + d->comboFriendsList->setDuplicatesEnabled(false); + + QPushButton *btnReload = new QPushButton(this); + btnReload->setToolTip(i18n("Reload friends list")); + btnReload->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); + btnReload->setMaximumWidth(25); + connect(btnReload, &QPushButton::clicked, this, &MastodonDMessageDialog::reloadFriendslist); + + QVBoxLayout *mainLayout = new QVBoxLayout(mainWidget); + + QHBoxLayout *toLayout = new QHBoxLayout; + toLayout->addWidget(lblTo); + toLayout->addWidget(d->comboFriendsList); + toLayout->addWidget(btnReload); + mainLayout->addLayout(toLayout); + + d->editor = new Choqok::UI::TextEdit(d->account->postCharLimit()); + connect(d->editor, &Choqok::UI::TextEdit::returnPressed, this, &MastodonDMessageDialog::submitPost); + mainLayout->addWidget(d->editor); + d->editor->setFocus(); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); + okButton->setText(i18nc("Send private message", "Send")); + connect(buttonBox, &QDialogButtonBox::accepted, this, &MastodonDMessageDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &MastodonDMessageDialog::reject); + mainLayout->addWidget(buttonBox); +} + +void MastodonDMessageDialog::setFriends(const QStringList friends) +{ + d->comboFriendsList->clear(); + d->comboFriendsList->addItems(friends); +} + +Choqok::UI::TextEdit *MastodonDMessageDialog::editor() +{ + return d->editor; +} + +MastodonAccount *MastodonDMessageDialog::account() +{ + return d->account; +} + +void MastodonDMessageDialog::reloadFriendslist() +{ + d->comboFriendsList->clear(); + MastodonMicroBlog *blog = qobject_cast(d->account->microblog()); + if (blog) { + connect(blog, &MastodonMicroBlog::followersUsernameListed, + this, &MastodonDMessageDialog::followersUsernameListed); + blog->fetchFollowers(d->account, true); + d->comboFriendsList->setCurrentText(i18n("Please wait...")); + } +} + +void MastodonDMessageDialog::accept() +{ + submitPost(d->editor->toPlainText()); +} + +void MastodonDMessageDialog::submitPost(QString text) +{ + if (d->account->following().isEmpty() || text.isEmpty() || d->comboFriendsList->currentText().isEmpty()) { + return; + } + hide(); + connect(d->account->microblog(), &Choqok::MicroBlog::errorPost, + this, &MastodonDMessageDialog::errorPost); + connect(d->account->microblog(), SIGNAL(postCreated(Choqok::Account*,Choqok::Post*)), + this, SLOT(postCreated(Choqok::Account*,Choqok::Post*))); + d->sentPost = new Choqok::Post; + d->sentPost->isPrivate = true; + d->sentPost->replyToUser.userName = d->comboFriendsList->currentText(); + d->sentPost->content = text; + d->account->microblog()->createPost(d->account, d->sentPost); +} + +void MastodonDMessageDialog::followersUsernameListed(MastodonAccount *theAccount, QStringList list) +{ + if (theAccount == d->account) { + d->comboFriendsList->clear(); + list.sort(Qt::CaseInsensitive); + d->comboFriendsList->addItems(list); + } +} + +void MastodonDMessageDialog::errorPost(Choqok::Account *theAccount, Choqok::Post *thePost, + Choqok::MicroBlog::ErrorType , QString , + Choqok::MicroBlog::ErrorLevel) +{ + if (theAccount == d->account && thePost == d->sentPost) { + qCDebug(CHOQOK); + show(); + } +} + +void MastodonDMessageDialog::setTo(const QString &username) +{ + d->comboFriendsList->setCurrentText(username); +} + diff --git a/microblogs/mastodon/mastodondmessagedialog.h b/microblogs/mastodon/mastodondmessagedialog.h new file mode 100644 index 00000000..0b139643 --- /dev/null +++ b/microblogs/mastodon/mastodondmessagedialog.h @@ -0,0 +1,72 @@ +/* + 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 MASTODONDMESSAGEDIALOG_H +#define MASTODONDMESSAGEDIALOG_H + +#include + +#include "microblog.h" + +namespace Choqok +{ +class Account; +class Post; + + namespace UI + { + class TextEdit; + } +} + +class MastodonAccount; + +class MastodonDMessageDialog : public QDialog +{ + Q_OBJECT +public: + explicit MastodonDMessageDialog(MastodonAccount *theAccount, QWidget *parent = 0, Qt::WindowFlags flags = 0); + ~MastodonDMessageDialog(); + void setTo(const QString &username); + +protected Q_SLOTS: + virtual void accept() override; + + void followersUsernameListed(MastodonAccount *, QStringList); + void submitPost(QString); + void reloadFriendslist(); + void errorPost(Choqok::Account *, Choqok::Post *, Choqok::MicroBlog::ErrorType, + QString, Choqok::MicroBlog::ErrorLevel); + +protected: + void setupUi(QWidget *mainWidget); + void setFriends(const QStringList friends); + Choqok::UI::TextEdit *editor(); + MastodonAccount *account(); + +private: + class Private; + Private *const d; +}; + +#endif // MASTODONDMESSAGEDIALOG_H diff --git a/microblogs/mastodon/mastodoneditaccountwidget.cpp b/microblogs/mastodon/mastodoneditaccountwidget.cpp new file mode 100644 index 00000000..c35579fe --- /dev/null +++ b/microblogs/mastodon/mastodoneditaccountwidget.cpp @@ -0,0 +1,222 @@ +/* + 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 + +#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, &QPushButton::clicked, this, &MastodonEditAccountWidget::authorizeUser); + + if (m_account) { + kcfg_alias->setText(m_account->alias()); + kcfg_acct->setText(m_account->username()); + 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->setUsername(MastodonMicroBlog::userNameFromAcct(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(MastodonMicroBlog::userNameFromAcct(kcfg_acct->text())); + m_account->setHost(QLatin1String("https://") + MastodonMicroBlog::hostFromAcct(kcfg_acct->text())); + + 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("https://choqok.kde.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, &KIO::StoredTransferJob::result, &loop, &QEventLoop::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..8a777820 --- /dev/null +++ b/microblogs/mastodon/mastodonmicroblog.cpp @@ -0,0 +1,990 @@ +/* + 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 "choqokappearancesettings.h" +#include "choqokbehaviorsettings.h" +#include "notifymanager.h" +#include "postwidget.h" + +#include "mastodonaccount.h" +#include "mastodoncomposerwidget.h" +#include "mastodondebug.h" +#include "mastodondmessagedialog.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")); +const QString MastodonMicroBlog::favouritesTimeline(QLatin1String("/api/v1/favourites")); + +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") << QLatin1String("Favourites"); + 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; + } +} + +Choqok::UI::ComposerWidget *MastodonMicroBlog::createComposerWidget(Choqok::Account *account, QWidget *parent) +{ + return new MastodonComposerWidget(account, parent); +} + +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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost); + 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 = status[QLatin1String("url")].toUrl(); + 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()) { + const QString client = application[QLatin1String("name")].toString(); + if (application[QLatin1String("website")].toString().isEmpty()) { + p->source = client; + } else { + p->source = QStringLiteral("%2").arg(application[QLatin1String("website")].toString()).arg(client); + } + } + + if (var[QLatin1String("visibility")].toString().compare(QLatin1String("direct")) == 0) { + p->isPrivate = true; + } + + QVariantMap account = status[QLatin1Literal("account")].toMap(); + + p->author.userId = account[QLatin1String("id")].toString(); + p->author.userName = account[QLatin1String("acct")].toString(); + p->author.realName = account[QLatin1String("display_name")].toString(); + p->author.homePageUrl = account[QLatin1String("url")].toUrl(); + + QTextDocument description; + description.setHtml(account[QLatin1String("note")].toString()); + p->author.description = description.toPlainText().trimmed(); + + p->author.profileImageUrl = account[QLatin1String("avatar")].toUrl(); + + p->replyToPostId = var[QLatin1String("in_reply_to_id")].toString(); + p->replyToUser.userId = 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(); + const QVariantMap repeatedFrom = var[QLatin1Literal("account")].toMap(); + p->repeatedFromUser.userId = repeatedFrom[QLatin1String("id")].toString(); + p->repeatedFromUser.userName = repeatedFrom[QLatin1String("acct")].toString(); + p->repeatedFromUser.homePageUrl = repeatedFrom[QLatin1String("url")].toUrl(); + } + + return p; + } else { + qCDebug(CHOQOK) << "post is not a MastodonPost!"; + return post; + } +} + +void MastodonMicroBlog::createReply(Choqok::Account *theAccount, MastodonPost *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QVariantMap object; + object.insert(QLatin1String("status"), post->content); + + if (!post->replyToPostId.isEmpty()) { + object.insert(QLatin1String("in_reply_to_id"), post->replyToPostId); + } + + 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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + +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->repeatedFromUser.userName) == 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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotReblog); + 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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFavorite); + 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; + + t = new Choqok::TimelineInfo; + t->name = i18nc("Timeline Name", "Favourites"); + t->description = i18nc("Timeline description", "Favourites"); + t->icon = QLatin1String("favorites"); + m_timelinesInfos[QLatin1String("Favourites")] = t; + m_timelinesPaths[QLatin1String("Favourites")] = favouritesTimeline; +} + +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, &KIO::TransferJob::result, this, &MastodonMicroBlog::slotRemovePost); + 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", QUrl()); + 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", QUrl()); + st->author.homePageUrl = grp.readEntry("authorHomePageUrl", QUrl()); + st->isRead = grp.readEntry("isRead", true); + st->conversationId = grp.readEntry("conversationId", QString()); + st->replyToPostId = grp.readEntry("replyToPostId", QString()); + st->replyToUser.userId = grp.readEntry("replyToUserId", QString()); + st->repeatedFromUser.userId = grp.readEntry("repeatedFromUserId", QString()); + st->repeatedFromUser.userName = grp.readEntry("repeatedFromUserName", QString()); + st->repeatedFromUser.homePageUrl = grp.readEntry("repeatedFromUserHomePage", QUrl()); + 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 QString &username) const +{ + if (username.contains(QLatin1Char('@'))) { + return QUrl::fromUserInput(QStringLiteral("https://%1/@%2").arg(hostFromAcct(username)).arg(userNameFromAcct(username))); + } else { + MastodonAccount *acc = qobject_cast(account); + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(QLatin1String("/@") + username); + + return url; + } +} + +QString MastodonMicroBlog::generateRepeatedByUserTooltip(const QString &username) const +{ + if (Choqok::AppearanceSettings::showRetweetsInChoqokWay()) { + return i18n("Boost of %1", username); + } else { + return i18n("Boosted by %1", username); + } +} + +void MastodonMicroBlog::showDirectMessageDialog(MastodonAccount *theAccount, const QString &toUsername) +{ + qCDebug(CHOQOK); + if (!theAccount) { + QAction *act = qobject_cast(sender()); + theAccount = qobject_cast( + Choqok::AccountManager::self()->findAccount(act->data().toString())); + } + MastodonDMessageDialog *dmsg = new MastodonDMessageDialog(theAccount, Choqok::UI::Global::mainWindow()); + if (!toUsername.isEmpty()) { + dmsg->setTo(toUsername); + } + dmsg->show(); +} + +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->replyToUser.userId); + grp.writeEntry("repeatedFromUserId", post->repeatedFromUser.userId); + grp.writeEntry("repeatedFromUserName", post->repeatedFromUser.userName); + grp.writeEntry("repeatedFromUserHomePage", post->repeatedFromUser.homePageUrl); + 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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotUpdateTimeline); + 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.toDisplayString().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, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFetchPost); + 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 for account %1 submitted successfully.", + theAccount->alias())); + 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); + } +} + +void MastodonMicroBlog::fetchFollowers(MastodonAccount* theAccount, bool active) +{ + qCDebug(CHOQOK); + QUrl url(theAccount->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/followers").arg(theAccount->id())); + + QUrlQuery urlQuery; + urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80")); + url.setQuery(urlQuery); + + 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(theAccount)); + mJobsAccount[job] = theAccount; + if (active) { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNameActive); + } else { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNamePassive); + } + job->start(); + Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating followers list for account %1...", + theAccount->alias())); +} + +void MastodonMicroBlog::slotRequestFollowersScreenNameActive(KJob* job) +{ + finishRequestFollowersScreenName(job, true); +} + +void MastodonMicroBlog::slotRequestFollowersScreenNamePassive(KJob* job) +{ + finishRequestFollowersScreenName(job, false); +} + +void MastodonMicroBlog::finishRequestFollowersScreenName(KJob *job, bool active) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low; + MastodonAccount *account = qobject_cast(mJobsAccount.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, ServerError, i18n("Followers list for account %1 could not be updated:\n%2", + account->username(), job->errorString()), level); + return; + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QByteArray buffer = j->data(); + const QJsonDocument json = QJsonDocument::fromJson(buffer); + if (!json.isNull()) { + QStringList followers; + for (const QVariant &user: json.array().toVariantList()) { + followers.append(user.toMap()[QLatin1String("acct")].toString()); + } + + account->setFollowers(followers); + } else { + QString err = i18n("Retrieving the followers list failed. The data returned from the server is corrupted."); + qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer; + Q_EMIT error(account, ParsingError, err, Critical); + } + } +} + +void MastodonMicroBlog::fetchFollowing(MastodonAccount* theAccount, bool active) +{ + qCDebug(CHOQOK); + QUrl url(theAccount->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/following").arg(theAccount->id())); + + QUrlQuery urlQuery; + urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80")); + url.setQuery(urlQuery); + + 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(theAccount)); + mJobsAccount[job] = theAccount; + if (active) { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNameActive); + } else { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNamePassive); + } + job->start(); + Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating following list for account %1...", + theAccount->alias())); +} + +void MastodonMicroBlog::slotRequestFollowingScreenNameActive(KJob* job) +{ + finishRequestFollowingScreenName(job, true); +} + +void MastodonMicroBlog::slotRequestFollowingScreenNamePassive(KJob* job) +{ + finishRequestFollowingScreenName(job, false); +} + +void MastodonMicroBlog::finishRequestFollowingScreenName(KJob *job, bool active) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low; + MastodonAccount *account = qobject_cast(mJobsAccount.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, ServerError, i18n("Following list for account %1 could not be updated:\n%2", + account->username(), job->errorString()), level); + return; + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QByteArray buffer = j->data(); + const QJsonDocument json = QJsonDocument::fromJson(buffer); + if (!json.isNull()) { + QStringList following; + for (const QVariant &user: json.array().toVariantList()) { + following.append(user.toMap()[QLatin1String("acct")].toString()); + } + + account->setFollowing(following); + } else { + QString err = i18n("Retrieving the following list failed. The data returned from the server is corrupted."); + qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer; + Q_EMIT error(account, ParsingError, err, Critical); + } + } +} + +#include "mastodonmicroblog.moc" diff --git a/microblogs/mastodon/mastodonmicroblog.h b/microblogs/mastodon/mastodonmicroblog.h new file mode 100644 index 00000000..d4308bce --- /dev/null +++ b/microblogs/mastodon/mastodonmicroblog.h @@ -0,0 +1,148 @@ +/* + 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 Choqok::UI::ComposerWidget *createComposerWidget(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 QString generateRepeatedByUserTooltip(const QString &username) const; + + virtual QUrl profileUrl(Choqok::Account *account, const QString &username) 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 createReply(Choqok::Account *theAccount, MastodonPost *post); + + void toggleReblog(Choqok::Account *theAccount, Choqok::Post *post); + + void toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post); + + void fetchFollowers(MastodonAccount *theAccount, bool active); + + void fetchFollowing(MastodonAccount *theAccount, bool active); + + static QString userNameFromAcct(const QString &acct); + static QString hostFromAcct(const QString &acct); + +Q_SIGNALS: + void favorite(Choqok::Account *, Choqok::Post *); + void followersUsernameListed(MastodonAccount *theAccount, const QStringList &friendsList); + void followingUsernameListed(MastodonAccount *theAccount, const QStringList &friendsList); + +public Q_SLOTS: + virtual void showDirectMessageDialog(MastodonAccount *theAccount = 0, + const QString &toUsername = QString()); + + void slotRequestFollowersScreenNameActive(KJob *job); + void slotRequestFollowersScreenNamePassive(KJob *job); + + void slotRequestFollowingScreenNameActive(KJob *job); + void slotRequestFollowingScreenNamePassive(KJob *job); + +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; + static const QString favouritesTimeline; + + 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(); + + void finishRequestFollowersScreenName(KJob *job, bool active); + + void finishRequestFollowingScreenName(KJob *job, bool active); + + QMap m_accountJobs; + QMap m_createPostJobs; + QMap m_favoriteJobs; + QMap m_removePostJobs; + QMap m_shareJobs; + QMap m_timelinesInfos; + QHash > m_timelinesLatestIds; + QMap mJobsAccount; + 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..93f939a1 --- /dev/null +++ b/microblogs/mastodon/mastodonpostwidget.cpp @@ -0,0 +1,201 @@ +/* + 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) { + ss += QStringLiteral("%1 - ").arg(getUsernameHyperlink(currentPost()->author)); + + QDateTime time; + if (post->repeatedDateTime.isNull()) { + time = post->creationDateTime; + } else { + time = post->repeatedDateTime; + } + + ss += QStringLiteral("%3").arg(post->link.toDisplayString()) + .arg(post->creationDateTime.toString(Qt::DefaultLocaleLongDate)) + .arg(formatDateTime(time)); + + if (!post->source.isEmpty()) { + ss += QLatin1String(" - ") + post->source; + } + + //ReTweet detection + if (!currentPost()->repeatedFromUser.userName.isEmpty()) { + const QString retweet = QLatin1String("
") + + microblog->generateRepeatedByUserTooltip(QStringLiteral("%2") + .arg(currentPost()->repeatedFromUser.homePageUrl.toDisplayString()) + .arg(microblog->userNameFromAcct(currentPost()->repeatedFromUser.userName))); + ss.append(retweet); + } + } else { + qCDebug(CHOQOK) << "post is not a MastodonPost!"; + } + + return ss; +} + +QString MastodonPostWidget::getUsernameHyperlink(const Choqok::User &user) const +{ + return QStringLiteral("%3") + .arg(user.homePageUrl.toDisplayString()) + .arg(user.description.isEmpty() ? user.realName : user.description.toHtmlEscaped()) + .arg(MastodonMicroBlog::userNameFromAcct(user.userName)); +} + +void MastodonPostWidget::initUi() +{ + Choqok::UI::PostWidget::initUi(); + + if (isResendAvailable()) { + buttons().value(QLatin1String("btnResend"))->setToolTip(i18nc("@info:tooltip", "Boost")); + } + + QPushButton *btnRe = addButton(QLatin1String("btnReply"), i18nc("@info:tooltip", "Reply"), QLatin1String("edit-undo")); + connect(btnRe, &QPushButton::clicked, this, &MastodonPostWidget::slotReply); + QMenu *menu = new QMenu(btnRe); + btnRe->setMenu(menu); + + QAction *actRep = new QAction(QIcon::fromTheme(QLatin1String("edit-undo")), i18n("Reply to %1", currentPost()->author.userName), menu); + menu->addAction(actRep); + menu->setDefaultAction(actRep); + connect(actRep, &QAction::triggered, this, &MastodonPostWidget::slotReply); + + QAction *actWrite = new QAction(QIcon::fromTheme(QLatin1String("document-edit")), i18n("Write to %1", currentPost()->author.userName), menu); + menu->addAction(actWrite); + connect(actWrite, &QAction::triggered, this, &MastodonPostWidget::slotWriteTo); + + if (!currentPost()->isPrivate) { + QAction *actReplytoAll = new QAction(i18n("Reply to all"), menu); + menu->addAction(actReplytoAll); + connect(actReplytoAll, &QAction::triggered, this, &MastodonPostWidget::slotReplyToAll); + } + + d->btnFavorite = addButton(QLatin1String("btnFavorite"), i18nc("@info:tooltip", "Favourite"), QLatin1String("rating")); + d->btnFavorite->setCheckable(true); + connect(d->btnFavorite, &QPushButton::clicked, this, &MastodonPostWidget::toggleFavorite); + updateFavStat(); +} + +void MastodonPostWidget::slotReply() +{ + setReadWithSignal(); + if (currentPost()->isPrivate) { + MastodonAccount *account = qobject_cast(currentAccount()); + MastodonMicroBlog *microblog = qobject_cast(account->microblog()); + microblog->showDirectMessageDialog(account, currentPost()->author.userName); + } else { + QString replyto = QStringLiteral("@%1").arg(currentPost()->author.userName); + QString postId = currentPost()->postId; + QString username = currentPost()->author.userName; + if (!currentPost()->repeatedFromUser.userName.isEmpty()) { + replyto.prepend(QStringLiteral("@%1 ").arg(currentPost()->repeatedFromUser.userName)); + postId = currentPost()->repeatedPostId; + } + Q_EMIT reply(replyto, postId, username); + } +} + +void MastodonPostWidget::slotWriteTo() +{ + Q_EMIT reply(QStringLiteral("@%1").arg(currentPost()->author.userName), QString(), currentPost()->author.userName); +} + +void MastodonPostWidget::slotReplyToAll() +{ + QString txt = QStringLiteral("@%1").arg(currentPost()->author.userName); + Q_EMIT reply(txt, currentPost()->postId, currentPost()->author.userName); +} + +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, &MastodonMicroBlog::favorite, this, &MastodonPostWidget::slotToggleFavorite); + 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..3457ec58 --- /dev/null +++ b/microblogs/mastodon/mastodonpostwidget.h @@ -0,0 +1,60 @@ +/* + 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 slotReply(); + void slotWriteTo(); + void slotReplyToAll(); + void toggleFavorite(); + +protected: + virtual QString getUsernameHyperlink(const Choqok::User &user) const; + + static const QIcon unFavIcon; + +private: + void updateFavStat(); + + class Private; + Private *const d; +}; + +#endif // MASTODONPOSTWIDGET_H diff --git a/microblogs/twitter/twitteraccount.cpp b/microblogs/twitter/twitteraccount.cpp index de091dcf..56644675 100644 --- a/microblogs/twitter/twitteraccount.cpp +++ b/microblogs/twitter/twitteraccount.cpp @@ -1,129 +1,129 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2008-2012 Mehrdad Momeny 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 "twitteraccount.h" #include "twitterdebug.h" #include "twittermicroblog.h" class TwitterAccount::Private { public: QString uploadHost; QUrl uploadUrl; // QStringList lists; }; -const char *twitterConsumerKey = "VyXMf0O7CvciiUQjliYtYg"; -const char *twitterConsumerSecret = "uD2HvsOBjzt1Vs6SnouFtuxDeHmvOOVwmn3fBVyCw0"; +static const char *twitterConsumerKey = "VyXMf0O7CvciiUQjliYtYg"; +static const char *twitterConsumerSecret = "uD2HvsOBjzt1Vs6SnouFtuxDeHmvOOVwmn3fBVyCw0"; TwitterAccount::TwitterAccount(TwitterMicroBlog *parent, const QString &alias) : TwitterApiAccount(parent, alias), d(new Private) { setHost(QLatin1String("https://api.twitter.com")); setUploadHost(QLatin1String("https://api.twitter.com")); setApi(QLatin1String("1.1")); qCDebug(CHOQOK) << "Set API version to 1.1"; setOauthConsumerKey(twitterConsumerKey); setOauthConsumerSecret(twitterConsumerSecret); setUsingOAuth(true); setPostCharLimit(280); //TODO: See if we can ask twitter for the char limit and make it dynamic if (!oauthToken().isEmpty() && !oauthTokenSecret().isEmpty()) { // We trigger this to update the username parent->verifyCredentials(this); } // d->lists = configGroup()->readEntry("lists", QStringList()); QStringList lists; for (const QString &tm: timelineNames()) { if (tm.startsWith(QLatin1Char('@'))) { lists.append(tm); } } if (!lists.isEmpty()) { parent->setListTimelines(this, lists); } } TwitterAccount::~TwitterAccount() { delete d; } void TwitterAccount::setApi(const QString &api) { TwitterApiAccount::setApi(api); generateUploadUrl(); } QUrl TwitterAccount::uploadUrl() const { return d->uploadUrl; } void TwitterAccount::setUploadUrl(const QUrl &url) { d->uploadUrl = url; } QString TwitterAccount::uploadHost() const { return d->uploadHost; } void TwitterAccount::setUploadHost(const QString &uploadHost) { d->uploadHost = uploadHost; } void TwitterAccount::generateUploadUrl() { if (!uploadHost().startsWith(QLatin1String("http"))) { //NOTE: This is for compatibility by prev versions. remove it after 1.0 release setUploadHost(uploadHost().prepend(QLatin1String("http://"))); } QUrl url(uploadHost()); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + (api())); setUploadUrl(url); } /* void TwitterAccount::writeConfig() { qCDebug(CHOQOK)<lists; configGroup()->writeEntry("lists", d->lists); TwitterApiAccount::writeConfig(); } void TwitterAccount::addList(const QString& name) { d->lists << name; } void TwitterAccount::removeList(const QString& name) { d->lists.removeOne(name); }*/