Show webfinger if displayName is unknown for an author.
[larjonas-pumpa.git] / src / pumpapp.cpp
blob06655def07780c0704d857117b68a5486fba02d8
1 /*
2 Copyright 2013-2015 Mats Sjöberg
4 This file is part of the Pumpa programme.
6 Pumpa is free software: you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 Pumpa is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
14 License for more details.
16 You should have received a copy of the GNU General Public License
17 along with Pumpa. If not, see <http://www.gnu.org/licenses/>.
20 #include <QStatusBar>
21 #include <QPalette>
22 #include <QInputDialog>
23 #include <QLineEdit>
24 #include <QClipboard>
25 #include <QDesktopServices>
27 #include "pumpapp.h"
29 #include "json.h"
30 #include "util.h"
31 #include "filedownloader.h"
32 #include "qaspell.h"
33 #include "editprofiledialog.h"
35 //------------------------------------------------------------------------------
37 PumpApp::PumpApp(PumpaSettings* settings, QString locale, QWidget* parent) :
38 QMainWindow(parent),
39 m_nextRequestId(0),
40 m_s(settings),
41 m_isLoading(false),
42 m_wiz(NULL),
43 m_messageWindow(NULL),
44 m_editProfileDialog(NULL),
45 m_trayIcon(NULL),
46 m_locale(locale),
47 m_uploadDialog(NULL),
48 m_uploadRequest(NULL)
50 if (m_locale.isEmpty())
51 m_locale = "en_US";
52 #ifdef USE_ASPELL
53 QASpell::setLocale(m_locale);
54 #endif
56 resize(m_s->size());
57 move(m_s->pos());
58 QASActor::setHiddenAuthors(m_s->hideAuthors());
60 // for old users set use_markdown=true, false for new installs
61 if (!m_s->firstStart() && !m_s->contains("General/use_markdown")) {
62 qDebug() << "Setting Markdown on by default for old users.";
63 m_s->useMarkdown(true);
66 m_settingsDialog = new PumpaSettingsDialog(m_s, this);
67 connect(m_settingsDialog, SIGNAL(newAccount()),
68 this, SLOT(launchOAuthWizard()));
70 QString linkColorStr = m_s->linkColor();
71 if (!linkColorStr.isEmpty()) {
72 QColor linkColor(linkColorStr);
73 if (linkColor.isValid()) {
74 QPalette pal(qApp->palette());
75 pal.setColor(QPalette::Link, linkColor);
76 pal.setColor(QPalette::LinkVisited, linkColor);
77 qApp->setPalette(pal);
78 } else {
79 qDebug() << "[ERROR] cannot parse link_color \"" + linkColorStr + "\"";
83 m_nam = new QNetworkAccessManager(this);
84 connect(m_nam, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)),
85 this, SLOT(onSslErrors(QNetworkReply*, QList<QSslError>)));
87 m_oam = new KQOAuthManager(this);
88 connect(m_oam, SIGNAL(authorizedRequestReady(QByteArray, int)),
89 this, SLOT(onAuthorizedRequestReady(QByteArray, int)));
90 connect(m_oam, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)),
91 this, SLOT(onSslErrors(QNetworkReply*, QList<QSslError>)));
93 m_fdm = FileDownloadManager::getManager(this);
95 createActions();
96 createMenu();
98 #ifdef USE_DBUS
99 m_dbus = new QDBusInterface("org.freedesktop.Notifications",
100 "/org/freedesktop/Notifications",
101 "org.freedesktop.Notifications");
102 if (!m_dbus->isValid()) {
103 qDebug() << "Unable to to connect to org.freedesktop.Notifications "
104 "dbus service.";
105 m_dbus = NULL;
107 #endif
109 updateTrayIcon();
110 connect(m_s, SIGNAL(trayIconChanged()), this, SLOT(updateTrayIcon()));
112 m_notifyMap = new QSignalMapper(this);
114 int max_tl = m_s->maxTimelineItems();
115 int max_fh = m_s->maxFirehoseItems();
117 m_tabWidget = new TabWidget(this);
118 m_tabWidget->setFocusPolicy(Qt::NoFocus);
120 m_inboxWidget = new CollectionWidget(this, max_tl);
121 connectCollection(m_inboxWidget);
123 m_inboxMinorWidget = new CollectionWidget(this, max_tl);
124 connectCollection(m_inboxMinorWidget);
126 m_directMajorWidget = new CollectionWidget(this, max_tl);
127 connectCollection(m_directMajorWidget);
129 m_directMinorWidget = new CollectionWidget(this, max_tl);
130 connectCollection(m_directMinorWidget);
132 m_favouritesWidget = new ObjectListWidget(m_tabWidget);
133 connectCollection(m_favouritesWidget);
134 m_favouritesWidget->hide();
136 m_followersWidget = new ObjectListWidget(m_tabWidget);
137 connectCollection(m_followersWidget);
138 m_followersWidget->hide();
140 m_followingWidget = new ObjectListWidget(m_tabWidget);
141 connectCollection(m_followingWidget, false);
142 m_followingWidget->hide();
144 m_userActivitiesWidget = new CollectionWidget(this, max_tl);
145 connectCollection(m_userActivitiesWidget, false);
146 m_userActivitiesWidget->hide();
148 m_firehoseWidget = new CollectionWidget(this, max_fh, 0);
149 connectCollection(m_firehoseWidget);
150 m_firehoseWidget->hide();
152 connect(m_inboxMinorWidget, SIGNAL(hasNewObjects()),
153 this, SLOT(onNewMinorObjects()));
154 connect(m_directMinorWidget, SIGNAL(hasNewObjects()),
155 this, SLOT(onNewMinorObjects()));
157 connect(m_tabWidget, SIGNAL(currentChanged(int)),
158 this, SLOT(tabSelected(int)));
160 m_tabWidget->addTab(m_inboxWidget, tr("&Inbox"));
161 m_tabWidget->addTab(m_directMinorWidget, tr("&Mentions"));
162 m_tabWidget->addTab(m_directMajorWidget, tr("&Direct"));
163 m_tabWidget->addTab(m_inboxMinorWidget, tr("Mean&while"));
164 // m_tabWidget->addTab(m_firehoseWidget, tr("Fi&rehose"), true, true);
166 m_notifyMap->setMapping(m_inboxWidget, FEED_INBOX);
167 m_notifyMap->setMapping(m_directMinorWidget, FEED_MENTIONS);
168 m_notifyMap->setMapping(m_directMajorWidget, FEED_DIRECT);
169 m_notifyMap->setMapping(m_inboxMinorWidget, FEED_MEANWHILE);
171 connect(m_notifyMap, SIGNAL(mapped(int)),
172 this, SLOT(timelineHighlighted(int)));
174 m_loadIcon = new QLabel(this);
175 m_loadMovie = new QMovie(":/images/loader.gif", QByteArray(), this);
176 statusBar()->addPermanentWidget(m_loadIcon);
178 setWindowTitle(CLIENT_FANCY_NAME);
179 setWindowIcon(QIcon(CLIENT_ICON));
180 setCentralWidget(m_tabWidget);
182 // oaRequest->setEnableDebugOutput(true);
183 // syncOAuthInfo();
185 m_timerId = -1;
187 if (!haveOAuth())
188 launchOAuthWizard();
189 else
190 startPumping();
193 //------------------------------------------------------------------------------
195 PumpApp::~PumpApp() {
196 m_s->size(size());
197 m_s->pos(pos());
198 m_s->hideAuthors(QASActor::getHiddenAuthors());
201 //------------------------------------------------------------------------------
203 void PumpApp::launchOAuthWizard() {
204 qApp->setQuitOnLastWindowClosed(false);
206 if (!m_wiz) {
207 m_wiz = new OAuthWizard(m_nam, m_oam, this);
208 connect(m_wiz, SIGNAL(clientRegistered(QString, QString, QString, QString)),
209 this, SLOT(onClientRegistered(QString, QString, QString, QString)));
210 connect(m_wiz, SIGNAL(accessTokenReceived(QString, QString)),
211 this, SLOT(onAccessTokenReceived(QString, QString)));
212 connect(m_wiz, SIGNAL(accepted()), this, SLOT(show()));
213 connect(m_wiz, SIGNAL(rejected()), this, SLOT(wizardCancelled()));
215 m_wiz->restart();
216 m_wiz->show();
219 //------------------------------------------------------------------------------
221 QString certSubjectInfo(const QSslCertificate& cert) {
222 #ifdef QT5
223 return cert.subjectInfo(QSslCertificate::CommonName).join(" ");
224 #else
225 return cert.subjectInfo(QSslCertificate::CommonName);
226 #endif
229 QString certIssuerInfo(const QSslCertificate& cert) {
230 #ifdef QT5
231 return cert.issuerInfo(QSslCertificate::CommonName).join(" ");
232 #else
233 return cert.issuerInfo(QSslCertificate::CommonName);
234 #endif
237 //------------------------------------------------------------------------------
239 void PumpApp::onSslErrors(QNetworkReply* reply, QList<QSslError> errors) {
240 if (m_s->ignoreSslErrors()) {
241 reply->ignoreSslErrors();
242 return;
245 QString infoText;
246 if (reply)
247 infoText += "URL: " + reply->url().toString() + "\n";
249 for (int i=0; i<errors.size(); i++) {
250 infoText += tr("SSL Error: ") + errors[i].errorString() + ".\n";
252 infoText +=
253 QString(tr("\n%1 is unable to verify the identity of the server. "
254 "This error could mean that someone is trying to impersonate the "
255 "server, or that the server's administrator has made an error.\n")).
256 arg(CLIENT_FANCY_NAME);
258 QString detailText;
259 QSslCertificate cert = errors[0].certificate();
260 if (!cert.isNull()) {
261 detailText = tr("SSL Server certificate.\n") +
262 tr("Issued to: ") + certSubjectInfo(cert) + "\n" +
263 tr("Issued by: ") + certIssuerInfo(cert) + "\n" +
264 tr("Effective: ") + cert.effectiveDate().toString() + "\n" +
265 tr("Expires: ") + cert.expiryDate().toString() + "\n" +
266 tr("MD5 digest: ") + cert.digest().toHex() + "\n";
269 qDebug() << infoText;
270 qDebug() << detailText;
272 QMessageBox msgBox;
273 msgBox.setText(tr("<b>Untrusted SSL connection!</b>"));
274 msgBox.setIcon(QMessageBox::Critical);
275 msgBox.setInformativeText(infoText);
276 if (!detailText.isEmpty())
277 msgBox.setDetailedText(detailText);
278 msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Abort);
279 msgBox.setDefaultButton(QMessageBox::Abort);
281 if (msgBox.exec() == QMessageBox::Ignore) {
282 reply->ignoreSslErrors();
283 return;
287 //------------------------------------------------------------------------------
289 void PumpApp::startPumping() {
290 resetActivityStreams();
292 QString webFinger = siteUrlToAccountId(m_s->userName(), m_s->siteUrl());
294 setWindowTitle(QString("%1 - %2").arg(CLIENT_FANCY_NAME).arg(webFinger));
296 // Setup endpoints for our timeline widgets
297 m_inboxWidget->setEndpoint(inboxEndpoint("major"), this, QAS_FOLLOW);
298 m_inboxMinorWidget->setEndpoint(inboxEndpoint("minor"), this);
299 m_directMajorWidget->setEndpoint(inboxEndpoint("direct/major"), this);
300 m_directMinorWidget->setEndpoint(inboxEndpoint("direct/minor"), this);
301 m_followersWidget->setEndpoint(apiUrl(apiUser("followers")), this);
302 m_followingWidget->setEndpoint(apiUrl(apiUser("following")), this,
303 QAS_FOLLOW);
304 m_favouritesWidget->setEndpoint(apiUrl(apiUser("favorites")), this);
305 m_firehoseWidget->setEndpoint(m_s->firehoseUrl(), this);
306 m_userActivitiesWidget->setEndpoint(apiUrl(apiUser("feed")), this);
307 show();
309 m_recipientLists.clear();
311 addPublicRecipient(m_recipientLists);
313 QVariantMap followersJson;
314 followersJson["displayName"] = tr("Followers");
315 followersJson["objectType"] = "collection";
316 followersJson["id"] = apiUrl(apiUser("followers"));
317 m_recipientLists.append(QASObject::getObject(followersJson, this));
319 request(apiUser("profile"), QAS_SELF_PROFILE);
320 request(apiUser("lists/person"), QAS_SELF_LISTS);
321 fetchAll(true);
323 resetTimer();
326 //------------------------------------------------------------------------------
328 void PumpApp::connectCollection(ASWidget* w, bool highlight) {
329 connect(w, SIGNAL(request(QString, int)), this, SLOT(request(QString, int)));
330 connect(w, SIGNAL(newReply(QASObject*, QASObjectList*, QASObjectList*)),
331 this, SLOT(newNote(QASObject*, QASObjectList*, QASObjectList*)));
332 connect(w, SIGNAL(linkHovered(const QString&)),
333 this, SLOT(statusMessage(const QString&)));
334 connect(w, SIGNAL(like(QASObject*)), this, SLOT(onLike(QASObject*)));
335 connect(w, SIGNAL(share(QASObject*)), this, SLOT(onShare(QASObject*)));
336 if (highlight)
337 connect(w, SIGNAL(highlightMe()), m_notifyMap, SLOT(map()));
338 connect(w, SIGNAL(showContext(QASObject*)),
339 this, SLOT(onShowContext(QASObject*)));
340 connect(w, SIGNAL(follow(QString, bool)), this, SLOT(follow(QString, bool)));
341 connect(w, SIGNAL(deleteObject(QASObject*)),
342 this, SLOT(onDeleteObject(QASObject*)));
343 connect(w, SIGNAL(editObject(QASObject*)),
344 this, SLOT(onEditObject(QASObject*)));
347 //------------------------------------------------------------------------------
349 void PumpApp::onClientRegistered(QString userName, QString siteUrl,
350 QString clientId, QString clientSecret) {
351 m_s->userName(userName);
352 m_s->siteUrl(siteUrl);
353 m_s->clientId(clientId);
354 m_s->clientSecret(clientSecret);
357 //------------------------------------------------------------------------------
359 void PumpApp::onAccessTokenReceived(QString token, QString tokenSecret) {
360 m_s->token(token);
361 m_s->tokenSecret(tokenSecret);
362 // syncOAuthInfo();
364 startPumping();
367 //------------------------------------------------------------------------------
369 bool PumpApp::haveOAuth() {
370 return !m_s->clientId().isEmpty() &&
371 !m_s->clientSecret().isEmpty() &&
372 !m_s->token().isEmpty() &&
373 !m_s->tokenSecret().isEmpty();
376 //------------------------------------------------------------------------------
378 void PumpApp::tabSelected(int index) {
379 m_tabWidget->deHighlightTab(index);
380 resetNotifications();
381 m_closeTabAction->setEnabled(m_tabWidget->closable(index));
384 //------------------------------------------------------------------------------
386 void PumpApp::timerEvent(QTimerEvent* event) {
387 if (event->timerId() != m_timerId)
388 return;
389 m_timerCount++;
391 if (m_timerCount >= m_s->reloadTime()) {
392 m_timerCount = 0;
393 fetchAll(false);
396 refreshTimeLabels();
399 //------------------------------------------------------------------------------
401 void PumpApp::resetTimer() {
402 if (m_timerId != -1)
403 killTimer(m_timerId);
404 m_timerId = startTimer(60*1000); // one minute timer
405 m_timerCount = 0;
408 //------------------------------------------------------------------------------
410 void PumpApp::debugAction() {
411 checkMemory("debug");
412 qDebug() << "inbox" << m_inboxWidget->count();
413 qDebug() << "meanwhile" << m_inboxMinorWidget->count();
414 qDebug() << "firehose" << m_firehoseWidget->count();
416 m_fdm->dumpStats();
419 //------------------------------------------------------------------------------
421 void PumpApp::refreshTimeLabels() {
422 m_inboxWidget->refreshTimeLabels();
423 m_directMinorWidget->refreshTimeLabels();
424 m_directMajorWidget->refreshTimeLabels();
425 m_inboxMinorWidget->refreshTimeLabels();
426 m_firehoseWidget->refreshTimeLabels();
427 for (int i=0; i<m_contextWidgets.size(); ++i)
428 m_contextWidgets[i]->refreshTimeLabels();
431 //------------------------------------------------------------------------------
433 void PumpApp::statusMessage(const QString& msg) {
434 statusBar()->showMessage(msg);
437 //------------------------------------------------------------------------------
439 void PumpApp::notifyMessage(QString msg) {
440 statusMessage(msg);
441 // qDebug() << "[STATUS]:" << msg;
444 //------------------------------------------------------------------------------
446 void PumpApp::timelineHighlighted(int feed) {
447 bool doTrayIcon = (feed & m_s->highlightFeeds()) && m_trayIcon;
448 bool doPopup = feed & m_s->popupFeeds();
450 // If we don't do any notifications don't even bother...
451 if (!doTrayIcon && !doPopup)
452 return;
454 // We highlight the tray icon and generate popups only on certain
455 // actions, so we first need to filter the list of new activities.
456 QList<QASActivity*> acts;
458 CollectionWidget* cw =
459 qobject_cast<CollectionWidget*>(m_notifyMap->mapping(feed));
460 if (!cw) // if it wasn't a regular timeline we ignore it
461 return;
463 // We just keep posts, i.e. new notes or comments.
464 // Other possibilities would be: follow favorite like
465 QStringList keepVerbs;
466 keepVerbs << "post";
468 // Filter: keep only activities that have a verb in keepVerbs
469 const QList<QASAbstractObject*>& ol = cw->newObjects();
470 for (int i=0; i<ol.count(); ++i) {
471 QASActivity* act = qobject_cast<QASActivity*>(ol.at(0));
472 if (act && keepVerbs.contains(act->verb()))
473 acts.push_back(act);
476 if (acts.isEmpty())
477 return;
479 // Highlight tray icon.
480 if (doTrayIcon)
481 m_trayIcon->setIcon(QIcon(":/images/pumpa_glow.png"));
483 // Popup notifications.
484 if (doPopup) {
485 int actsCount = 0;
486 for (int i=0; i<acts.size(); i++)
487 if (!acts.at(i)->skipNotify())
488 actsCount++;
490 if (actsCount == 0)
491 return;
493 QString msg =
494 QString(tr("You have %Ln new notification(s).", 0, actsCount));
496 // If there's only a single post activity we'll make the
497 // notification more informative.
498 QASActivity* act = acts.at(0);
499 QASObject* obj = act->object();
500 QASActor* actor = act->actor();
501 if (actsCount == 1 && act->verb() == "post" && obj && actor) {
502 QString actorName = actor->displayNameOrWebFinger();
503 if (obj->type() == "comment")
504 msg = QString(tr("%1 commented: ")).arg(actorName);
505 else
506 msg = QString(tr("%1 wrote: ")).arg(actorName);
507 msg += "\"" + obj->excerpt() + "\"";
509 sendNotification(CLIENT_FANCY_NAME, msg);
513 //------------------------------------------------------------------------------
515 void PumpApp::onNewMinorObjects() {
516 CollectionWidget* cw = qobject_cast<CollectionWidget*>(sender());
517 if (!cw)
518 return;
520 const QList<QASAbstractObject*>& newObjects = cw->newObjects();
521 cw->url();
523 for (int i=0; i<newObjects.size(); ++i) {
524 QASActivity* act = qobject_cast<QASActivity*>(newObjects.at(i));
525 if (act && act->object() && act->object()->inReplyTo()) {
526 QASObject* irtObj = act->object()->inReplyTo();
527 if (irtObj->url().isEmpty() || isShown(irtObj))
528 refreshObject(irtObj);
533 //------------------------------------------------------------------------------
535 void PumpApp::resetNotifications() {
536 if (m_trayIcon)
537 m_trayIcon->setIcon(QIcon(CLIENT_ICON));
538 m_tabWidget->deHighlightTab();
541 //------------------------------------------------------------------------------
543 bool PumpApp::sendNotification(QString summary, QString text) {
544 #ifdef USE_DBUS
545 if (m_dbus && m_dbus->isValid()) {
547 // https://developer.gnome.org/notification-spec/
548 QList<QVariant> args;
549 args.append(CLIENT_NAME); // Application Name
550 args.append(0123U); // Replaces ID (0U)
551 args.append(QString()); // Notification Icon
552 args.append(summary); // Summary
553 args.append(text); // Body
554 args.append(QStringList()); // Actions
556 QVariantMap hints;
557 // for hints to make icon, see
558 // https://dev.visucore.com/bitcoin/doxygen/notificator_8cpp_source.html
559 args.append(hints);
560 args.append(3000);
562 m_dbus->callWithArgumentList(QDBus::NoBlock, "Notify", args);
563 return true;
565 #endif
567 if (QSystemTrayIcon::supportsMessages() && m_trayIcon) {
568 m_trayIcon->showMessage(CLIENT_FANCY_NAME, summary+" "+text);
569 return true;
572 qDebug() << "[NOTIFY]" << summary << text;
573 return false;
576 //------------------------------------------------------------------------------
578 void PumpApp::errorMessage(QString msg) {
579 statusMessage(tr("Error: ") + msg);
580 qDebug() << "[ERROR]:" << msg;
583 //------------------------------------------------------------------------------
585 void PumpApp::updateTrayIcon() {
586 bool useTray = m_s->useTrayIcon() && QSystemTrayIcon::isSystemTrayAvailable();
588 if (useTray) {
589 qApp->setQuitOnLastWindowClosed(false);
590 if (!m_trayIcon)
591 createTrayIcon();
592 else
593 m_trayIcon->show();
595 if (m_trayIcon) {
596 QString toolTip = CLIENT_FANCY_NAME;
597 if (!m_s->userName().isEmpty())
598 toolTip += " - " + siteUrlToAccountId(m_s->userName(), m_s->siteUrl());
599 m_trayIcon->setToolTip(toolTip);
601 } else {
602 qApp->setQuitOnLastWindowClosed(true);
603 if (m_trayIcon)
604 m_trayIcon->hide();
608 //------------------------------------------------------------------------------
610 void PumpApp::createTrayIcon() {
611 m_trayIconMenu = new QMenu(this);
612 m_trayIconMenu->addAction(newNoteAction);
613 // m_trayIconMenu->addAction(newPictureAction);
614 m_trayIconMenu->addSeparator();
615 m_trayIconMenu->addAction(m_showHideAction);
616 m_trayIconMenu->addAction(exitAction);
618 m_trayIcon = new QSystemTrayIcon(QIcon(CLIENT_ICON));
619 connect(m_trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
620 this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason)));
621 m_trayIcon->setContextMenu(m_trayIconMenu);
622 m_trayIcon->setToolTip(CLIENT_FANCY_NAME);
623 m_trayIcon->show();
626 //------------------------------------------------------------------------------
628 void PumpApp::trayIconActivated(QSystemTrayIcon::ActivationReason reason) {
629 if (reason == QSystemTrayIcon::Trigger) {
630 m_trayIcon->setIcon(QIcon(CLIENT_ICON));
631 toggleVisible();
635 //------------------------------------------------------------------------------
637 QString PumpApp::showHideText(bool visible) {
638 return QString(tr("%1 &Window")).arg(visible ? tr("Hide") : tr("Show") );
641 //------------------------------------------------------------------------------
643 void PumpApp::toggleVisible() {
644 setVisible(!isVisible());
645 m_showHideAction->setText(showHideText());
646 if (isVisible())
647 activateWindow();
650 //------------------------------------------------------------------------------
652 void PumpApp::createActions() {
653 exitAction = new QAction(tr("E&xit"), this);
654 exitAction->setShortcut(tr("Ctrl+Q"));
655 connect(exitAction, SIGNAL(triggered()), this, SLOT(exit()));
657 openPrefsAction = new QAction(tr("Preferences"), this);
658 connect(openPrefsAction, SIGNAL(triggered()), this, SLOT(preferences()));
660 reloadAction = new QAction(tr("&Reload timeline"), this);
661 reloadAction->setShortcut(tr("Ctrl+R"));
662 connect(reloadAction, SIGNAL(triggered()),
663 this, SLOT(reload()));
665 loadOlderAction = new QAction(tr("Load older in timeline"), this);
666 loadOlderAction->setShortcut(tr("Ctrl+O"));
667 connect(loadOlderAction, SIGNAL(triggered()), this, SLOT(loadOlder()));
669 followAction = new QAction(tr("F&ollow an account"), this);
670 followAction->setShortcut(tr("Ctrl+L"));
671 connect(followAction, SIGNAL(triggered()), this, SLOT(followDialog()));
673 profileAction = new QAction(tr("Your &profile"), this);
674 connect(profileAction, SIGNAL(triggered()), this, SLOT(editProfile()));
676 aboutAction = new QAction(tr("&About"), this);
677 connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
679 aboutQtAction = new QAction(tr("About &Qt"), this);
680 connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
682 reportBugAction = new QAction(tr("Report &bug online"), this);
683 connect(reportBugAction, SIGNAL(triggered()), this, SLOT(reportBug()));
685 newNoteAction = new QAction(tr("New &Note"), this);
686 newNoteAction->setShortcut(tr("Ctrl+N"));
687 connect(newNoteAction, SIGNAL(triggered()), this, SLOT(newNote()));
689 m_debugAction = new QAction("Debug", this);
690 m_debugAction->setShortcut(tr("Ctrl+D"));
691 connect(m_debugAction, SIGNAL(triggered()), this, SLOT(debugAction()));
692 addAction(m_debugAction);
694 m_closeTabAction = new QAction(tr("Close tab"), this);
695 m_closeTabAction->setShortcut(tr("Ctrl+W"));
696 connect(m_closeTabAction, SIGNAL(triggered()), this, SLOT(closeTab()));
697 m_closeTabAction->setEnabled(false);
699 m_firehoseAction = new QAction(tr("Firehose"), this);
700 connect(m_firehoseAction, SIGNAL(triggered()), this, SLOT(showFirehose()));
702 m_followersAction = new QAction(tr("Followers"), this);
703 connect(m_followersAction, SIGNAL(triggered()), this, SLOT(showFollowers()));
705 m_followingAction = new QAction(tr("Following"), this);
706 connect(m_followingAction, SIGNAL(triggered()), this, SLOT(showFollowing()));
708 m_favouritesAction = new QAction(tr("Favorites"), this);
709 connect(m_favouritesAction, SIGNAL(triggered()),
710 this, SLOT(showFavourites()));
712 m_userActivitiesAction = new QAction(tr("Activities"), this);
713 connect(m_userActivitiesAction, SIGNAL(triggered()),
714 this, SLOT(showUserActivities()));
716 m_showHideAction = new QAction(showHideText(true), this);
717 connect(m_showHideAction, SIGNAL(triggered()), this, SLOT(toggleVisible()));
720 //------------------------------------------------------------------------------
722 void PumpApp::createMenu() {
723 fileMenu = new QMenu(tr("&Pumpa"), this);
724 fileMenu->addAction(newNoteAction);
725 fileMenu->addSeparator();
726 fileMenu->addAction(followAction);
727 fileMenu->addAction(profileAction);
728 fileMenu->addAction(reloadAction);
729 fileMenu->addAction(loadOlderAction);
730 fileMenu->addSeparator();
731 fileMenu->addAction(openPrefsAction);
732 fileMenu->addSeparator();
733 fileMenu->addAction(exitAction);
734 menuBar()->addMenu(fileMenu);
736 m_tabsMenu = new QMenu(tr("&Tabs"), this);
737 m_tabsMenu->addAction(m_userActivitiesAction);
738 m_tabsMenu->addAction(m_favouritesAction);
739 m_tabsMenu->addAction(m_followersAction);
740 m_tabsMenu->addAction(m_followingAction);
741 m_tabsMenu->addAction(m_firehoseAction);
742 m_tabsMenu->addAction(m_closeTabAction);
743 menuBar()->addMenu(m_tabsMenu);
745 helpMenu = new QMenu(tr("&Help"), this);
746 helpMenu->addAction(aboutAction);
747 helpMenu->addAction(aboutQtAction);
748 helpMenu->addSeparator();
749 helpMenu->addAction(reportBugAction);
750 menuBar()->addMenu(helpMenu);
753 //------------------------------------------------------------------------------
755 void PumpApp::preferences() {
756 m_settingsDialog->exec();
759 //------------------------------------------------------------------------------
761 void PumpApp::wizardCancelled() {
762 qApp->setQuitOnLastWindowClosed(true);
763 if (!haveOAuth())
764 exit();
767 //------------------------------------------------------------------------------
769 void PumpApp::exit() {
770 qApp->exit();
773 //------------------------------------------------------------------------------
775 void PumpApp::about() {
776 static const QString GPL =
777 tr("<p>Pumpa is free software: you can redistribute it and/or modify it "
778 "under the terms of the GNU General Public License as published by "
779 "the Free Software Foundation, either version 3 of the License, or "
780 "(at your option) any later version.</p>"
781 "<p>Pumpa is distributed in the hope that it will be useful, but "
782 "WITHOUT ANY WARRANTY; without even the implied warranty of "
783 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU "
784 "General Public License for more details.</p>"
785 "<p>You should have received a copy of the GNU General Public License "
786 "along with Pumpa. If not, see "
787 "<a href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>."
788 "</p>");
789 static const QString credits =
790 tr("<p>The <a href=\"https://github.com/kypeli/kQOAuth\">kQOAuth library"
791 "</a> is copyrighted by <a href=\"http://www.johanpaul.com/\">Johan "
792 "Paul</a> and licensed under LGPL 2.1.</p>"
793 "<p>The <a href=\"https://github.com/vmg/sundown\">sundown Markdown "
794 "library</a> is copyrighted by Natacha Port&eacute;, Vicent Marti and "
795 "others, and <a href=\"https://github.com/vmg/sundown#license\">"
796 "permissively licensed</a>.</p>"
797 "<p>The Pumpa logo was "
798 "<a href=\"http://opengameart.org/content/fruit-and-veggie-inventory\">"
799 "created by Joshua Taylor</a> for the "
800 "<a href=\"http://lpc.opengameart.org/\">Liberated Pixel Cup</a>."
801 "The logo is copyrighted by the artist and is dual licensed under the "
802 "CC-BY-SA 3.0 license and the GNU GPL 3.0.");
804 QString mainText =
805 QString("<p><b>%1 %2</b> - %3<br/><a href=\"%4\">%4</a><br/>"
806 + tr("Copyright &copy; 2013-2015 Mats Sj&ouml;berg")
807 + " - <a href=\"https://pump.saz.im/sazius\">sazius@pump.saz.im</a>."
808 "</p>"
809 + tr("<p>Report bugs and feature requests at "
810 "<a href=\"%5\">%5</a>.</p>"))
811 .arg(CLIENT_FANCY_NAME)
812 .arg(CLIENT_VERSION)
813 .arg(tr("A simple Qt-based pump.io client."))
814 .arg(WEBSITE_URL)
815 .arg(BUGTRACKER_URL);
817 QMessageBox::about(this, QString(tr("About %1")).arg(CLIENT_FANCY_NAME),
818 mainText + GPL + credits);
821 //------------------------------------------------------------------------------
823 void PumpApp::reportBug() {
824 QDesktopServices::openUrl(QString(BUGTRACKER_URL));
827 //------------------------------------------------------------------------------
829 void PumpApp::addPublicRecipient(RecipientList& rl) {
830 QVariantMap publicJson;
831 publicJson["displayName"] = tr("Public");
832 publicJson["objectType"] = "collection";
833 publicJson["id"] = PUBLIC_RECIPIENT_ID;
834 rl.append(QASObject::getObject(publicJson, this));
837 //------------------------------------------------------------------------------
839 void PumpApp::newNote(QASObject* obj, QASObjectList* to, QASObjectList* cc,
840 bool edit) {
841 if (!m_messageWindow) {
842 m_messageWindow = new MessageWindow(m_s, &m_recipientLists, this);
843 connect(m_messageWindow,
844 SIGNAL(sendMessage(QString, QString, RecipientList, RecipientList)),
845 this,
846 SLOT(postNote(QString, QString, RecipientList, RecipientList)));
847 connect(m_messageWindow, SIGNAL(sendImage(QString, QString, QString,
848 RecipientList, RecipientList)),
849 this, SLOT(postImage(QString, QString, QString,
850 RecipientList, RecipientList)));
851 connect(m_messageWindow, SIGNAL(sendReply(QASObject*, QString,
852 RecipientList, RecipientList)),
853 this, SLOT(postReply(QASObject*, QString,
854 RecipientList, RecipientList)));
855 connect(m_messageWindow, SIGNAL(sendEdit(QASObject*, QString, QString)),
856 this, SLOT(postEdit(QASObject*, QString, QString)));
857 m_messageWindow->setCompletions(&m_completions);
859 if (edit)
860 m_messageWindow->editMessage(obj);
861 else
862 m_messageWindow->newMessage(obj, to, cc);
863 m_messageWindow->show();
866 //------------------------------------------------------------------------------
868 void PumpApp::reload() {
869 fetchAll(true);
870 refreshTimeLabels();
873 //------------------------------------------------------------------------------
875 void PumpApp::fetchAll(bool all) {
876 m_inboxWidget->fetchNewer();
877 if (m_inboxWidget->linksInitialised())
878 m_inboxWidget->refresh();
879 m_directMinorWidget->fetchNewer();
880 m_directMajorWidget->fetchNewer();
881 m_inboxMinorWidget->fetchNewer();
883 if (tabShown(m_firehoseWidget))
884 m_firehoseWidget->fetchNewer();
886 for (int i=0; i<m_contextWidgets.size(); ++i)
887 m_contextWidgets[i]->fetchNewer();
889 // These will be reloaded even if not shown, if all=true
890 if (all || tabShown(m_followersWidget))
891 m_followersWidget->fetchNewer();
892 if (all || tabShown(m_followingWidget))
893 m_followingWidget->fetchNewer();
894 if (all || tabShown(m_favouritesWidget))
895 m_favouritesWidget->fetchNewer();
896 if (all || tabShown(m_userActivitiesWidget))
897 m_userActivitiesWidget->fetchNewer();
900 //------------------------------------------------------------------------------
902 void PumpApp::loadOlder() {
903 ASWidget* cw =
904 qobject_cast<ASWidget*>(m_tabWidget->currentWidget());
905 if (cw)
906 cw->fetchOlder();
909 //------------------------------------------------------------------------------
911 bool PumpApp::isShown(QASAbstractObject* obj) {
912 // check context widgets first
913 for (int i=0; i<m_contextWidgets.size(); ++i)
914 if (m_contextWidgets[i]->hasObject(obj))
915 return true;
917 // check all other tab widgets
918 return m_inboxWidget->hasObject(obj) ||
919 m_directMinorWidget->hasObject(obj) ||
920 m_directMajorWidget->hasObject(obj) ||
921 m_inboxMinorWidget->hasObject(obj) ||
922 (tabShown(m_firehoseWidget) && m_firehoseWidget->hasObject(obj)) ||
923 (tabShown(m_followersWidget) && m_followersWidget->hasObject(obj)) ||
924 (tabShown(m_followingWidget) && m_followingWidget->hasObject(obj)) ||
925 (tabShown(m_favouritesWidget) && m_favouritesWidget->hasObject(obj)) ||
926 (tabShown(m_userActivitiesWidget) &&
927 m_userActivitiesWidget->hasObject(obj));
930 //------------------------------------------------------------------------------
932 QString PumpApp::inboxEndpoint(QString path) {
933 if (m_s->siteUrl().isEmpty()) {
934 errorMessage(tr("Site not configured yet!"));
935 return "";
937 return m_s->siteUrl() + "/api/user/" + m_s->userName() + "/inbox/" + path;
940 //------------------------------------------------------------------------------
942 void PumpApp::onLike(QASObject* obj) {
943 feed(obj->liked() ? "unlike" : "like", obj->toJson(),
944 QAS_ACTIVITY | QAS_TOGGLE_LIKE);
947 //------------------------------------------------------------------------------
949 void PumpApp::onShare(QASObject* obj) {
950 feed("share", obj->toJson(), QAS_ACTIVITY | QAS_REFRESH);
953 //------------------------------------------------------------------------------
955 void PumpApp::errorBox(QString msg) {
956 QMessageBox::critical(this, CLIENT_FANCY_NAME, msg, QMessageBox::Ok);
959 //------------------------------------------------------------------------------
961 bool PumpApp::webFingerFromString(QString text, QString& username,
962 QString& server) {
963 if (text.startsWith("https://") || text.startsWith("http://")) {
964 int slashPos = text.lastIndexOf('/');
965 if (slashPos > 0)
966 text = siteUrlToAccountId(text.mid(slashPos+1), text.left(slashPos));
969 return splitWebfingerId(text, username, server);
972 //------------------------------------------------------------------------------
974 void PumpApp::followDialog() {
975 bool ok;
977 QString defaultText = "evan@e14n.com";
978 QString cbText = QApplication::clipboard()->text();
979 if (cbText.contains('@') || cbText.startsWith("https://") ||
980 cbText.startsWith("http://"))
981 defaultText = cbText;
983 QString text =
984 QInputDialog::getText(this, tr("Follow pump.io user"),
985 tr("Enter webfinger ID of person to follow: "),
986 QLineEdit::Normal, defaultText, &ok);
988 if (!ok || text.isEmpty())
989 return;
991 QString username, server;
992 QString error;
994 if (!webFingerFromString(text, username, server))
995 error = tr("Sorry, that doesn't even look like a webfinger ID!");
997 QASObject* obj = QASObject::getObject("acct:" + username + "@" + server);
998 QASActor* actor = obj ? obj->asActor() : NULL;
999 if (actor && actor->followed())
1000 error = tr("Sorry, you are already following that person!");
1002 if (!error.isEmpty())
1003 return errorBox(error);
1005 testUserAndFollow(username, server);
1008 //------------------------------------------------------------------------------
1010 void PumpApp::editProfile() {
1011 request(apiUser("profile"), QAS_EDIT_PROFILE);
1014 //------------------------------------------------------------------------------
1016 void PumpApp::editProfileDialog() {
1017 if (!m_editProfileDialog) {
1018 m_editProfileDialog = new EditProfileDialog(this);
1019 connect(m_editProfileDialog, SIGNAL(profileEdited(QASActor*, QString)),
1020 this, SLOT(onProfileEdited(QASActor*, QString)));
1022 m_editProfileDialog->setProfile(m_selfActor);
1023 m_editProfileDialog->show();
1026 //------------------------------------------------------------------------------
1028 void PumpApp::onProfileEdited(QASActor* profile, QString newImageFile) {
1029 m_profile.clear();
1031 m_profile["objectType"] = "person";
1032 m_profile["displayName"] = profile->displayName();
1033 m_profile["summary"] = profile->summary();
1035 QVariantMap jsonLoc;
1036 jsonLoc["objectType"] = "place";
1037 jsonLoc["displayName"] = profile->location();
1038 m_profile["location"] = jsonLoc;
1040 if (newImageFile.isEmpty()) {
1041 uploadProfile();
1042 } else {
1043 postAvatarImage(newImageFile);
1047 //------------------------------------------------------------------------------
1049 void PumpApp::uploadProfile() {
1050 request(apiUser("profile"), QAS_SELF_PROFILE | QAS_REFRESH,
1051 KQOAuthRequest::PUT, m_profile);
1054 //------------------------------------------------------------------------------
1056 void PumpApp::testUserAndFollow(QString username, QString server) {
1057 QString fingerUrl = QString("%1/.well-known/webfinger?resource=%2@%1").
1058 arg(server).arg(username);
1060 QNetworkRequest rec(QUrl("https://" + fingerUrl));
1061 QNetworkReply* reply = m_nam->head(rec);
1062 connect(reply, SIGNAL(finished()), this, SLOT(userTestDoneAndFollow()));
1063 qDebug() << "testUserAndFollow" << fingerUrl;
1065 // isn't this an ugly yet fancy hack? :-)
1066 reply->setProperty("pumpa_redirects", 0);
1069 //------------------------------------------------------------------------------
1071 void PumpApp::userTestDoneAndFollow() {
1072 QString error;
1074 QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
1075 QUrl url = reply->url();
1077 #ifdef QT5
1078 QUrlQuery replyQuery(url.query());
1079 QString userId = replyQuery.queryItemValue("resource");
1080 #else
1081 QString userId = url.queryItemValue("resource");
1082 #endif
1084 int redirs = reply->property("pumpa_redirects").toInt();
1085 #ifdef DEBUG_NET
1086 qDebug() << "userTestDoneAndFollow" << url << redirs;
1087 #endif
1089 if (reply->error() != QNetworkReply::NoError) {
1090 if (redirs == 0) {
1091 url.setScheme("http");
1092 QNetworkRequest rec(url);
1093 QNetworkReply* r = m_nam->head(rec);
1094 r->setProperty("pumpa_redirects", ++redirs);
1095 connect(r, SIGNAL(finished()), this, SLOT(userTestDoneAndFollow()));
1096 return;
1097 } else {
1098 return errorBox(tr("Invalid user: ") + userId);
1102 QUrl loc = reply->header(QNetworkRequest::LocationHeader).toUrl();
1103 if (loc.isValid()) {
1104 if (redirs > 5)
1105 return errorBox(tr("Invalid user (cannot check site): ") + userId);
1106 reply->deleteLater();
1108 QNetworkRequest rec(loc);
1109 QNetworkReply* r = m_nam->head(rec);
1110 r->setProperty("pumpa_redirects", ++redirs);
1111 connect(r, SIGNAL(finished()), this, SLOT(userTestDoneAndFollow()));
1112 return;
1115 follow("acct:" + userId, true);
1118 //------------------------------------------------------------------------------
1120 bool PumpApp::tabShown(ASWidget* aw) const {
1121 return aw && m_tabWidget->indexOf(aw) != -1;
1124 //------------------------------------------------------------------------------
1126 void PumpApp::onShowContext(QASObject* obj) {
1127 ContextWidget* cw = new ContextWidget(this);
1128 connectCollection(cw);
1130 m_tabWidget->addTab(cw, tr("&Context"), true, true);
1131 cw->setObject(obj);
1132 m_tabWidget->setCurrentWidget(cw);
1134 m_contextWidgets.append(cw);
1137 //------------------------------------------------------------------------------
1139 void PumpApp::closeTab() {
1140 ContextWidget* cw =
1141 qobject_cast<ContextWidget*>(m_tabWidget->closeCurrentTab());
1142 if (cw) {
1143 int i = m_contextWidgets.indexOf(cw);
1144 if (i != -1)
1145 m_contextWidgets.removeAt(i);
1146 delete cw;
1150 //------------------------------------------------------------------------------
1152 void PumpApp::showFirehose() {
1153 if (!tabShown(m_firehoseWidget))
1154 m_tabWidget->addTab(m_firehoseWidget, tr("Fi&rehose"), true, true);
1155 m_tabWidget->setCurrentWidget(m_firehoseWidget);
1156 m_firehoseWidget->fetchNewer();
1159 //------------------------------------------------------------------------------
1161 void PumpApp::showFollowers() {
1162 if (!tabShown(m_followersWidget))
1163 m_tabWidget->addTab(m_followersWidget, tr("&Followers"), true, true);
1164 m_tabWidget->setCurrentWidget(m_followersWidget);
1165 m_followersWidget->fetchNewer();
1168 //------------------------------------------------------------------------------
1170 void PumpApp::showFollowing() {
1171 if (!tabShown(m_followingWidget))
1172 m_tabWidget->addTab(m_followingWidget, tr("F&ollowing"), false, true);
1173 m_tabWidget->setCurrentWidget(m_followingWidget);
1174 m_followingWidget->fetchNewer();
1177 //------------------------------------------------------------------------------
1179 void PumpApp::showFavourites() {
1180 if (!tabShown(m_favouritesWidget))
1181 m_tabWidget->addTab(m_favouritesWidget, tr("F&avorites"), false, true);
1182 m_tabWidget->setCurrentWidget(m_favouritesWidget);
1183 m_favouritesWidget->fetchNewer();
1186 //------------------------------------------------------------------------------
1188 void PumpApp::showUserActivities() {
1189 if (!tabShown(m_userActivitiesWidget))
1190 m_tabWidget->addTab(m_userActivitiesWidget, tr("A&ctivities"), false, true);
1191 m_tabWidget->setCurrentWidget(m_userActivitiesWidget);
1192 m_userActivitiesWidget->fetchNewer();
1195 //------------------------------------------------------------------------------
1197 void PumpApp::postNote(QString content, QString title,
1198 RecipientList to, RecipientList cc) {
1199 if (content.isEmpty())
1200 return;
1202 QVariantMap obj;
1203 obj["objectType"] = "note";
1204 obj["content"] = addTextMarkup(content, m_s->useMarkdown());
1206 QString ptitle = processTitle(title, false);
1207 if (!ptitle.isEmpty())
1208 obj["displayName"] = ptitle;
1210 feed("post", obj, QAS_OBJECT | QAS_REFRESH | QAS_POST, to, cc);
1213 //------------------------------------------------------------------------------
1215 void PumpApp::postEdit(QASObject* obj, QString content, QString title) {
1216 QVariantMap json;
1217 json["id"] = obj->id();
1218 json["objectType"] = obj->type();
1219 json["content"] = addTextMarkup(content, m_s->useMarkdown());
1221 QString ptitle = processTitle(title, false);
1222 if (!ptitle.isEmpty())
1223 json["displayName"] = ptitle;
1225 feed("update", json, QAS_OBJECT | QAS_REFRESH | QAS_POST);
1228 //------------------------------------------------------------------------------
1230 void PumpApp::postImage(QString msg,
1231 QString title,
1232 QString imageFile,
1233 RecipientList to,
1234 RecipientList cc) {
1235 m_imageObject.clear();
1236 m_imageObject["content"] = addTextMarkup(msg, m_s->useMarkdown());
1237 m_imageObject["displayName"] = processTitle(title, false);
1239 m_imageTo = to;
1240 m_imageCc = cc;
1242 uploadFile(imageFile);
1245 //------------------------------------------------------------------------------
1247 void PumpApp::postAvatarImage(QString imageFile) {
1248 m_imageObject.clear();
1250 m_imageTo = RecipientList();
1251 m_imageCc = RecipientList();
1253 addPublicRecipient(m_imageCc);
1255 uploadFile(imageFile, QAS_AVATAR_UPLOAD);
1258 //------------------------------------------------------------------------------
1260 void PumpApp::uploadFile(QString filename, int flags) {
1261 QString lcfn = filename.toLower();
1262 QString mimeType;
1263 if (lcfn.endsWith(".jpg") || lcfn.endsWith(".jpeg"))
1264 mimeType = "image/jpeg";
1265 else if (lcfn.endsWith(".png"))
1266 mimeType = "image/png";
1267 else if (lcfn.endsWith(".gif"))
1268 mimeType = "image/gif";
1269 else {
1270 qDebug() << "Cannot determine mime type of file" << filename;
1271 return;
1274 QFile fp(filename);
1275 if (!fp.open(QIODevice::ReadOnly)) {
1276 qDebug() << "Unable to read file" << filename;
1277 return;
1280 QByteArray ba = fp.readAll();
1282 KQOAuthRequest* oaRequest = initRequest(apiUrl(apiUser("uploads")),
1283 KQOAuthRequest::POST);
1284 oaRequest->setContentType(mimeType);
1285 oaRequest->setContentLength(ba.size());
1286 oaRequest->setRawData(ba);
1288 if (m_uploadDialog == NULL) {
1289 m_uploadDialog = new QProgressDialog("Uploading image...", "Abort", 0, 100,
1290 this);
1291 m_uploadDialog->setWindowModality(Qt::WindowModal);
1292 connect(m_uploadDialog, SIGNAL(canceled()), this, SLOT(uploadCanceled()));
1293 } else {
1294 m_uploadDialog->reset();
1296 m_uploadDialog->setValue(0);
1297 m_uploadDialog->show();
1299 flags = QAS_IMAGE_UPLOAD | flags;
1300 m_uploadRequest = executeRequest(oaRequest, flags);
1301 connect(m_uploadRequest, SIGNAL(uploadProgress(qint64, qint64)),
1302 this, SLOT(uploadProgress(qint64, qint64)));
1305 //------------------------------------------------------------------------------
1307 void PumpApp::updatePostedImage(QVariantMap obj, int flags) {
1308 m_imageObject.unite(obj);
1310 // Work-around for https://github.com/e14n/pump.io/issues/885
1311 // Thanks to Owen Shepherd for pointing this out!
1312 RecipientList to;
1313 to.append(m_selfActor);
1315 feed("update", m_imageObject, QAS_IMAGE_UPDATE | flags, to);
1318 //------------------------------------------------------------------------------
1320 void PumpApp::postImageActivity(QVariantMap, int flags) {
1321 if (flags == 0)
1322 flags = QAS_REFRESH;
1324 flags |= QAS_ACTIVITY | QAS_POST;
1326 feed("post", m_imageObject, flags, m_imageTo, m_imageCc);
1329 //------------------------------------------------------------------------------
1331 void PumpApp::uploadProgress(qint64 bytesSent, qint64 bytesTotal) {
1332 if (!m_uploadDialog || bytesTotal <= 0)
1333 return;
1335 m_uploadDialog->setValue((100*bytesSent)/bytesTotal);
1338 //------------------------------------------------------------------------------
1340 void PumpApp::uploadCanceled(bool abortRequest) {
1341 if (m_uploadRequest && abortRequest) {
1342 #ifdef DEBUG_NET
1343 qDebug() << "[DEBUG] aborting upload...";
1344 #endif
1345 m_uploadRequest->abort();
1347 m_uploadRequest = NULL;
1349 m_imageObject.clear();
1350 m_uploadDialog->reset();
1353 //------------------------------------------------------------------------------
1355 void PumpApp::postReply(QASObject* replyToObj, QString content,
1356 RecipientList to, RecipientList cc) {
1357 if (content.isEmpty())
1358 return;
1360 QVariantMap obj;
1361 obj["objectType"] = "comment";
1362 obj["content"] = addTextMarkup(content, m_s->useMarkdown());
1364 QVariantMap noteObj;
1365 noteObj["id"] = replyToObj->id();
1366 noteObj["objectType"] = replyToObj->type();
1367 obj["inReplyTo"] = noteObj;
1369 feed("post", obj, QAS_ACTIVITY | QAS_REFRESH | QAS_POST, to, cc);
1372 //------------------------------------------------------------------------------
1374 void PumpApp::follow(QString acctId, bool follow) {
1375 QVariantMap obj;
1376 obj["id"] = acctId;
1377 obj["objectType"] = "person";
1379 int mode = QAS_ACTIVITY;
1380 if (follow)
1381 mode |= QAS_FOLLOW;
1382 else
1383 mode |= QAS_UNFOLLOW;
1385 feed(follow ? "follow" : "stop-following", obj, mode);
1388 //------------------------------------------------------------------------------
1390 void PumpApp::onDeleteObject(QASObject* obj) {
1391 QVariantMap json;
1392 json["id"] = obj->id();
1393 json["objectType"] = obj->type();
1395 feed("delete", json, QAS_ACTIVITY);
1398 //------------------------------------------------------------------------------
1400 void PumpApp::onEditObject(QASObject* obj) {
1401 newNote(obj, NULL, NULL, true);
1404 //------------------------------------------------------------------------------
1406 void PumpApp::addRecipient(QVariantMap& data, QString name, RecipientList to) {
1407 if (to.isEmpty())
1408 return;
1410 QVariantList recList;
1412 for (int i=0; i<to.size(); ++i) {
1413 QASObject* obj = to.at(i);
1415 QVariantMap rec;
1416 rec["objectType"] = obj->type();
1417 rec["id"] = obj->id();
1418 if (!obj->proxyUrl().isEmpty()) {
1419 QVariantMap pump_io;
1420 pump_io["proxyURL"] = obj->proxyUrl();
1421 rec["pump_io"] = pump_io;
1424 recList.append(rec);
1427 data[name] = recList;
1430 //------------------------------------------------------------------------------
1432 void PumpApp::feed(QString verb, QVariantMap object, int response_id,
1433 RecipientList to, RecipientList cc) {
1434 QString endpoint = "api/user/" + m_s->userName() + "/feed";
1436 QVariantMap data;
1437 data["verb"] = verb;
1438 data["object"] = object;
1440 addRecipient(data, "to", to);
1441 addRecipient(data, "cc", cc);
1443 request(endpoint, response_id, KQOAuthRequest::POST, data);
1446 //------------------------------------------------------------------------------
1448 QString PumpApp::apiUrl(QString endpoint) {
1449 QString ret = endpoint;
1450 if (!ret.startsWith("http")) {
1451 if (ret[0] != '/')
1452 ret = '/' + ret;
1453 ret = m_s->siteUrl() + ret;
1455 return ret;
1458 //------------------------------------------------------------------------------
1460 QString PumpApp::apiUser(QString path) {
1461 return QString("api/user/%1/%2").arg(m_s->userName()).arg(path);
1464 //------------------------------------------------------------------------------
1466 KQOAuthRequest* PumpApp::initRequest(QString endpoint,
1467 KQOAuthRequest::RequestHttpMethod method) {
1468 KQOAuthRequest* oaRequest = new KQOAuthRequest(this);
1469 oaRequest->initRequest(KQOAuthRequest::AuthorizedRequest, QUrl(endpoint));
1470 oaRequest->setConsumerKey(m_s->clientId());
1471 oaRequest->setConsumerSecretKey(m_s->clientSecret());
1472 oaRequest->setToken(m_s->token());
1473 oaRequest->setTokenSecret(m_s->tokenSecret());
1474 oaRequest->setHttpMethod(method);
1475 oaRequest->setTimeout(60000); // one minute time-out
1476 return oaRequest;
1479 //------------------------------------------------------------------------------
1481 void PumpApp::request(QString endpoint, int response_id,
1482 KQOAuthRequest::RequestHttpMethod method,
1483 QVariantMap data) {
1484 endpoint = apiUrl(endpoint);
1486 bool firehose = (endpoint == m_s->firehoseUrl());
1487 if (!endpoint.startsWith(m_s->siteUrl()) && !firehose) {
1488 #ifdef DEBUG_NET
1489 qDebug() << "[DEBUG] dropping request for" << endpoint;
1490 #endif
1491 return;
1494 #ifdef DEBUG_NET
1495 qDebug() << (method == KQOAuthRequest::GET ? "[GET]" :
1496 method == KQOAuthRequest::POST ? "[POST]" : "[PUT]")
1497 << response_id << ":" << endpoint;
1498 #endif
1500 QStringList epl = endpoint.split("?");
1501 KQOAuthRequest* oaRequest = initRequest(epl[0], method);
1503 // I have no idea why this is the only way that seems to
1504 // work. Incredibly frustrating and ugly :-/
1505 if (epl.size() > 1) {
1506 KQOAuthParameters params;
1507 QStringList parts = epl[1].split("&");
1508 for (int i=0; i<parts.size(); i++) {
1509 QStringList ps = parts[i].split("=");
1510 params.insert(ps[0], QUrl::fromPercentEncoding(ps[1].toLatin1()));
1512 oaRequest->setAdditionalParameters(params);
1515 if (method == KQOAuthRequest::POST || method == KQOAuthRequest::PUT) {
1516 QByteArray ba = serializeJson(data);
1517 oaRequest->setRawData(ba);
1518 oaRequest->setContentType("application/json");
1519 oaRequest->setContentLength(ba.size());
1520 #ifdef DEBUG_NET
1521 qDebug() << "DATA" << oaRequest->rawData();
1522 #endif
1525 executeRequest(oaRequest, response_id);
1527 if (!m_isLoading)
1528 notifyMessage(tr("Loading ..."));
1529 setLoading(true);
1532 //------------------------------------------------------------------------------
1534 QNetworkReply* PumpApp::executeRequest(KQOAuthRequest* request,
1535 int response_id) {
1536 int id = m_nextRequestId++;
1538 if (m_nextRequestId > 32000) { // bound to be smaller than any MAX_INT
1539 m_nextRequestId = 0;
1540 while (m_requestMap.contains(m_nextRequestId))
1541 m_nextRequestId++;
1544 m_requestMap.insert(id, qMakePair(request, response_id));
1545 m_oam->executeAuthorizedRequest(request, id);
1547 return m_oam->getReply(request);
1550 //------------------------------------------------------------------------------
1552 void PumpApp::followActor(QASActor* actor, bool doFollow) {
1553 actor->setFollowed(doFollow);
1555 QString from = QString("%1 (%2)").arg(actor->displayName()).
1556 arg(actor->webFinger());
1558 if (from.isEmpty() || from.startsWith("http://") ||
1559 from.startsWith("https://"))
1560 return;
1562 if (doFollow)
1563 m_completions.insert(from, actor);
1564 else
1565 m_completions.remove(from);
1568 //------------------------------------------------------------------------------
1570 void PumpApp::onAuthorizedRequestReady(QByteArray response, int rid) {
1571 KQOAuthManager::KQOAuthError lastError = m_oam->lastError();
1573 QPair<KQOAuthRequest*, int> rp = m_requestMap.take(rid);
1574 KQOAuthRequest* request = rp.first;
1575 int id = rp.second;
1576 QString reqUrl = request->requestEndpoint().toString();
1578 #ifdef DEBUG_NET_MOAR
1579 qDebug() << "[DEBUG] request done [" << rid << id << "]" << reqUrl
1580 << response.count() << "bytes";
1581 #endif
1582 #ifdef DEBUG_NET_EVEN_MOAR
1583 qDebug() << "[DEBUG]" << response;
1584 #endif
1586 request->deleteLater();
1588 if (m_requestMap.isEmpty()) {
1589 setLoading(false);
1590 notifyMessage(tr("Ready!"));
1592 #ifdef DEBUG_NET_MOAR
1593 else {
1594 qDebug() << "[DEBUG] Still waiting for requests:";
1595 QMapIterator<int, requestInfo_t> i(m_requestMap);
1596 while (i.hasNext()) {
1597 i.next();
1598 requestInfo_t ri = i.value();
1599 qDebug() << " " << ri.first->requestEndpoint() << ri.second;
1602 #endif
1604 int sid = id & 0xFF;
1606 if (lastError) {
1607 if (id & QAS_POST) {
1608 errorMessage(tr("Unable to post message!"));
1609 m_messageWindow->show();
1610 } else if (sid == QAS_IMAGE_UPLOAD) {
1611 uploadCanceled(false);
1612 errorMessage(tr("Unable to upload image!"));
1613 } else if (sid == QAS_OBJECT) {
1614 qDebug() << "[WARNING] unable to fetch context for object.";
1615 } else {
1616 errorMessage(QString(tr("Network or authorisation error [%1/%2] %3.")).
1617 arg(m_oam->lastError()).arg(id).arg(reqUrl));
1619 #ifdef DEBUG_NET
1620 qDebug() << "[ERROR]" << response;
1621 #endif
1622 return;
1625 QVariantMap json = parseJson(response);
1626 if (sid == QAS_NULL)
1627 return;
1629 if (sid == QAS_COLLECTION) {
1630 QASCollection::getCollection(json, this, id);
1631 } else if (sid == QAS_ACTIVITY) {
1632 QASActivity* act = QASActivity::getActivity(json, this);
1633 if (act) { // if not a broken activity
1634 QASObject* obj = act->object();
1636 if ((id & QAS_AVATAR_UPLOAD) && obj) {
1638 QVariantMap jsonImage;
1639 jsonImage["url"] = obj->imageUrl();
1640 jsonImage["width"] = 96;
1641 jsonImage["height"] = 96;
1642 m_profile["image"] = jsonImage;
1644 uploadProfile();
1647 if ((id & QAS_TOGGLE_LIKE) && obj)
1648 obj->toggleLiked();
1650 if ((id & QAS_FOLLOW) || (id & QAS_UNFOLLOW)) {
1651 QASActor* actor = obj ? obj->asActor() : NULL;
1652 if (actor) {
1653 bool doFollow = (id & QAS_FOLLOW);
1654 followActor(actor, doFollow);
1655 notifyMessage(QString(doFollow ? tr("Successfully followed ") :
1656 tr("Successfully unfollowed ")) +
1657 actor->displayNameOrWebFinger());
1661 } else if (sid == QAS_OBJECTLIST) {
1662 QASObjectList* ol = QASObjectList::getObjectList(json, this, id);
1663 if (ol && (id & QAS_FOLLOW)) {
1664 for (size_t i=0; i<ol->size(); ++i) {
1665 QASActor* actor = ol->at(i)->asActor();
1666 if (actor)
1667 followActor(actor);
1670 if (ol->nextLink().isEmpty())
1671 QASActor::setFollowedKnown();
1673 } else if (sid == QAS_OBJECT) {
1674 QASObject::getObject(json, this);
1675 } else if (sid == QAS_ACTORLIST) {
1676 QASActorList::getActorList(json, this);
1677 } else if (sid == QAS_SELF_PROFILE || sid == QAS_EDIT_PROFILE) {
1678 m_selfActor = QASActor::getActor(json, this);
1679 m_selfActor->setYou();
1680 if (sid == QAS_EDIT_PROFILE)
1681 editProfileDialog();
1682 } else if (sid == QAS_SELF_LISTS) {
1683 QASObjectList* lists = QASObjectList::getObjectList(json, this, id);
1684 for (size_t i=0; i<lists->size(); ++i) {
1685 m_recipientLists.append(lists->at(i));
1687 } else if (sid == QAS_IMAGE_UPLOAD) {
1688 m_uploadDialog->reset();
1689 updatePostedImage(json, id & QAS_AVATAR_UPLOAD);
1690 } else if (sid == QAS_IMAGE_UPDATE) {
1691 postImageActivity(json, id & QAS_AVATAR_UPLOAD);
1694 if ((id & QAS_POST) && m_messageWindow && !m_messageWindow->isVisible())
1695 m_messageWindow->clear();
1697 if (id & QAS_REFRESH) {
1698 fetchAll(false);
1702 //------------------------------------------------------------------------------
1703 // FIXME: this shouldn't be implemented in millions of places
1705 void PumpApp::refreshObject(QASAbstractObject* obj) {
1706 if (!obj)
1707 return;
1709 QDateTime now = QDateTime::currentDateTime();
1710 QDateTime lr = obj->lastRefreshed();
1712 if (lr.isNull() || lr.secsTo(now) > 10) {
1713 obj->lastRefreshed(now);
1714 request(obj->apiLink(), obj->asType());
1718 //------------------------------------------------------------------------------
1720 void PumpApp::setLoading(bool on) {
1721 if (!m_loadIcon || m_isLoading == on)
1722 return;
1724 m_isLoading = on;
1726 if (!on) {
1727 m_loadIcon->setMovie(NULL);
1728 m_loadIcon->setPixmap(QPixmap(":/images/empty.gif"));
1729 } else if (m_loadMovie->isValid()) {
1730 // m_loadIcon->setPixmap(QPixmap());
1731 m_loadIcon->setMovie(m_loadMovie);
1732 m_loadMovie->start();