File indexing completed on 2024-04-21 03:56:29

0001 /*
0002     SPDX-FileCopyrightText: 2019 Piyush Aggarwal <piyushaggarwal002@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "notifybysnore.h"
0008 #include "debug_p.h"
0009 #include "knotification.h"
0010 #include "knotifyconfig.h"
0011 #include "knotificationreplyaction.h"
0012 
0013 #include <QGuiApplication>
0014 #include <QIcon>
0015 #include <QLocalSocket>
0016 
0017 #include <snoretoastactions.h>
0018 
0019 /*
0020  * On Windows a shortcut to your app is needed to be installed in the Start Menu
0021  * (and subsequently, registered with the OS) in order to show notifications.
0022  * Since KNotifications is a library, an app using it can't (feasibly) be properly
0023  * registered with the OS. It is possible we could come up with some complicated solution
0024  * which would require every KNotification-using app to do some special and probably
0025  * difficult to understand change to support Windows. Or we can have SnoreToast.exe
0026  * take care of all that nonsense for us.
0027  * Note that, up to this point, there have been no special
0028  * KNotifications changes to the generic application codebase to make this work,
0029  * just some tweaks to the Craft blueprint and packaging script
0030  * to pull in SnoreToast and trigger shortcut building respectively.
0031  * Be sure to have a shortcut installed in Windows Start Menu by SnoreToast.
0032  *
0033  * So the location doesn't matter, but it's only possible to register the internal COM server in an executable.
0034  * We could make it a static lib and link it in all KDE applications,
0035  * but to make the action center integration work, we would need to also compile a class
0036  * into the executable using a compile time uuid.
0037  *
0038  * The used header is meant to help with parsing the response.
0039  * The cmake target for LibSnoreToast is a INTERFACE lib, it only provides the include path.
0040  *
0041  *
0042  * Trigger the shortcut installation during the installation of your app; syntax for shortcut installation is -
0043  * ./SnoreToast.exe -install <absolute\address\of\shortcut> <absolute\address\to\app.exe> <appID>
0044  *
0045  * appID: use as-is from your app's QCoreApplication::applicationName() when installing the shortcut.
0046  * NOTE: Install the shortcut in Windows Start Menu folder.
0047  * For example, check out Craft Blueprint for Quassel-IRC or KDE Connect.
0048  */
0049 
0050 namespace
0051 {
0052 const QString SnoreToastExecName()
0053 {
0054     return QStringLiteral("SnoreToast.exe");
0055 }
0056 }
0057 
0058 NotifyBySnore::NotifyBySnore(QObject *parent)
0059     : KNotificationPlugin(parent)
0060 {
0061     m_server.listen(QString::number(qHash(qApp->applicationDirPath())));
0062     connect(&m_server, &QLocalServer::newConnection, this, [this]() {
0063         QLocalSocket *responseSocket = m_server.nextPendingConnection();
0064         connect(responseSocket, &QLocalSocket::readyRead, [this, responseSocket]() {
0065             const QByteArray rawNotificationResponse = responseSocket->readAll();
0066             responseSocket->deleteLater();
0067 
0068             const QString notificationResponse = QString::fromWCharArray(reinterpret_cast<const wchar_t *>(rawNotificationResponse.constData()),
0069                                                                          rawNotificationResponse.size() / sizeof(wchar_t));
0070             qCDebug(LOG_KNOTIFICATIONS) << notificationResponse;
0071 
0072             QMap<QString, QStringView> notificationResponseMap;
0073             for (const auto str : QStringView(notificationResponse).split(QLatin1Char(';'))) {
0074                 const int equalIndex = str.indexOf(QLatin1Char('='));
0075                 notificationResponseMap.insert(str.mid(0, equalIndex).toString(), str.mid(equalIndex + 1));
0076             }
0077 
0078             const QString responseAction = notificationResponseMap[QStringLiteral("action")].toString();
0079             const int responseNotificationId = notificationResponseMap[QStringLiteral("notificationId")].toInt();
0080 
0081             qCDebug(LOG_KNOTIFICATIONS) << "The notification ID is : " << responseNotificationId;
0082 
0083             KNotification *notification;
0084             const auto iter = m_notifications.constFind(responseNotificationId);
0085             if (iter != m_notifications.constEnd()) {
0086                 notification = iter.value();
0087             } else {
0088                 qCWarning(LOG_KNOTIFICATIONS) << "Received a response for an unknown notification.";
0089                 return;
0090             }
0091 
0092             std::wstring w_action(responseAction.size(), 0);
0093             responseAction.toWCharArray(const_cast<wchar_t *>(w_action.data()));
0094 
0095             switch (SnoreToastActions::getAction(w_action)) {
0096             case SnoreToastActions::Actions::Clicked:
0097                 qCDebug(LOG_KNOTIFICATIONS) << "User clicked on the toast.";
0098                 break;
0099 
0100             case SnoreToastActions::Actions::Hidden:
0101                 qCDebug(LOG_KNOTIFICATIONS) << "The toast got hidden.";
0102                 break;
0103 
0104             case SnoreToastActions::Actions::Dismissed:
0105                 qCDebug(LOG_KNOTIFICATIONS) << "User dismissed the toast.";
0106                 break;
0107 
0108             case SnoreToastActions::Actions::Timedout:
0109                 qCDebug(LOG_KNOTIFICATIONS) << "The toast timed out.";
0110                 break;
0111 
0112             case SnoreToastActions::Actions::ButtonClicked: {
0113                 qCDebug(LOG_KNOTIFICATIONS) << "User clicked an action button in the toast.";
0114                 const QString responseButton = notificationResponseMap[QStringLiteral("button")].toString();
0115                 Q_EMIT actionInvoked(responseNotificationId, responseButton);
0116                 break;
0117             }
0118 
0119             case SnoreToastActions::Actions::TextEntered: {
0120                 qCWarning(LOG_KNOTIFICATIONS) << "User entered some text in the toast.";
0121                 const QString replyText = notificationResponseMap[QStringLiteral("text")].toString();
0122                 qCWarning(LOG_KNOTIFICATIONS) << "Text entered was :: " << replyText;
0123                 Q_EMIT replied(responseNotificationId, replyText);
0124                 break;
0125             }
0126 
0127             default:
0128                 qCWarning(LOG_KNOTIFICATIONS) << "Unexpected behaviour with the toast. Please file a bug report / feature request.";
0129                 break;
0130             }
0131 
0132             // Action Center callbacks are not yet supported so just close the notification once done
0133             if (notification != nullptr) {
0134                 NotifyBySnore::close(notification);
0135             }
0136         });
0137     });
0138 }
0139 
0140 NotifyBySnore::~NotifyBySnore()
0141 {
0142     m_server.close();
0143 }
0144 
0145 void NotifyBySnore::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
0146 {
0147     Q_UNUSED(notifyConfig);
0148     // HACK work around that notification->id() is only populated after returning from here
0149     // note that config will be invalid at that point, so we can't pass that along
0150     QMetaObject::invokeMethod(
0151         this,
0152         [this, notification]() {
0153             NotifyBySnore::notifyDeferred(notification);
0154         },
0155         Qt::QueuedConnection);
0156 }
0157 
0158 void NotifyBySnore::notifyDeferred(KNotification *notification)
0159 {
0160     m_notifications.insert(notification->id(), notification);
0161 
0162     const QString notificationTitle = ((!notification->title().isEmpty()) ? notification->title() : qApp->applicationDisplayName());
0163     QStringList snoretoastArgsList{QStringLiteral("-id"),
0164                                    QString::number(notification->id()),
0165                                    QStringLiteral("-t"),
0166                                    notificationTitle,
0167                                    QStringLiteral("-m"),
0168                                    stripRichText(notification->text()),
0169                                    QStringLiteral("-appID"),
0170                                    qApp->applicationName(),
0171                                    QStringLiteral("-pid"),
0172                                    QString::number(qApp->applicationPid()),
0173                                    QStringLiteral("-pipename"),
0174                                    m_server.fullServerName()};
0175 
0176     // handle the icon for toast notification
0177     const QString iconPath = m_iconDir.path() + QLatin1Char('/') + QString::number(notification->id());
0178     const bool hasIcon = (notification->pixmap().isNull()) ? qApp->windowIcon().pixmap(1024, 1024).save(iconPath, "PNG") //
0179                                                            : notification->pixmap().save(iconPath, "PNG");
0180     if (hasIcon) {
0181         snoretoastArgsList << QStringLiteral("-p") << iconPath;
0182     }
0183 
0184     // if'd below, because SnoreToast currently doesn't support both textbox and buttons in the same notification
0185     if (notification->replyAction()) {
0186         snoretoastArgsList << QStringLiteral("-tb");
0187     } else if (!notification->actions().isEmpty()) {
0188         // add actions if any
0189 
0190         const auto actions = notification->actions();
0191         QString actionsString;
0192 
0193         for (KNotificationAction *action : actions) {
0194             action->setId(action->label());
0195             actionsString += QLatin1Char(';') + action->label();
0196         }
0197 
0198         snoretoastArgsList << QStringLiteral("-b") << actionsString;
0199     }
0200 
0201     QProcess *snoretoastProcess = new QProcess();
0202     connect(snoretoastProcess, &QProcess::readyReadStandardError, [snoretoastProcess, snoretoastArgsList]() {
0203         const auto data = snoretoastProcess->readAllStandardError();
0204         qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process stderr:" << snoretoastArgsList << data;
0205     });
0206     connect(snoretoastProcess, &QProcess::readyReadStandardOutput, [snoretoastProcess, snoretoastArgsList]() {
0207         const auto data = snoretoastProcess->readAllStandardOutput();
0208         qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process stdout:" << snoretoastArgsList << data;
0209     });
0210     connect(snoretoastProcess, &QProcess::errorOccurred, this, [this, snoretoastProcess, snoretoastArgsList, iconPath](QProcess::ProcessError error) {
0211         qCWarning(LOG_KNOTIFICATIONS) << "SnoreToast process errored:" << snoretoastArgsList << error;
0212         snoretoastProcess->deleteLater();
0213         QFile::remove(iconPath);
0214     });
0215     connect(snoretoastProcess,
0216             qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
0217             this,
0218             [this, snoretoastProcess, snoretoastArgsList, iconPath](int exitCode, QProcess::ExitStatus exitStatus) {
0219                 qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process finished:" << snoretoastArgsList;
0220                 qCDebug(LOG_KNOTIFICATIONS) << "code:" << exitCode << "status:" << exitStatus;
0221                 snoretoastProcess->deleteLater();
0222                 QFile::remove(iconPath);
0223             });
0224 
0225     qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process starting:" << snoretoastArgsList;
0226     snoretoastProcess->start(SnoreToastExecName(), snoretoastArgsList);
0227 }
0228 
0229 void NotifyBySnore::close(KNotification *notification)
0230 {
0231     qCDebug(LOG_KNOTIFICATIONS) << "Requested to close notification with ID:" << notification->id();
0232     if (m_notifications.constFind(notification->id()) == m_notifications.constEnd()) {
0233         qCWarning(LOG_KNOTIFICATIONS) << "Couldn't find the notification in m_notifications. Nothing to close.";
0234         return;
0235     }
0236 
0237     m_notifications.remove(notification->id());
0238 
0239     const QStringList snoretoastArgsList{QStringLiteral("-close"), QString::number(notification->id()), QStringLiteral("-appID"), qApp->applicationName()};
0240 
0241     qCDebug(LOG_KNOTIFICATIONS) << "Closing notification; SnoreToast process arguments:" << snoretoastArgsList;
0242     QProcess::startDetached(SnoreToastExecName(), snoretoastArgsList);
0243 
0244     finish(notification);
0245 }
0246 
0247 void NotifyBySnore::update(KNotification *notification, const KNotifyConfig &notifyConfig)
0248 {
0249     Q_UNUSED(notification);
0250     Q_UNUSED(notifyConfig);
0251     qCWarning(LOG_KNOTIFICATIONS) << "updating a notification is not supported yet.";
0252 }
0253 
0254 #include "moc_notifybysnore.cpp"