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 ¬ifyConfig) 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 ¬ifyConfig) 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"