/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/linux/notifications_manager_linux.h" #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION #include "base/platform/base_platform_info.h" #include "platform/linux/specific_linux.h" #include "core/application.h" #include "core/core_settings.h" #include "history/history.h" #include "main/main_session.h" #include "lang/lang_keys.h" #include #include #include #include #include #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION namespace Platform { namespace Notifications { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION namespace { constexpr auto kService = "org.freedesktop.Notifications"_cs; constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs; constexpr auto kInterface = kService; constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; bool NotificationsSupported = false; bool InhibitedNotSupported = false; void ComputeSupported(bool wait = false) { const auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("GetServerInformation")); auto async = QDBusConnection::sessionBus().asyncCall(message); auto watcher = new QDBusPendingCallWatcher(async); QObject::connect( watcher, &QDBusPendingCallWatcher::finished, [=](QDBusPendingCallWatcher *call) { QDBusPendingReply reply = *call; if (reply.isValid()) { NotificationsSupported = true; } call->deleteLater(); }); if (wait) { watcher->waitForFinished(); } } void GetSupported() { static auto Checked = false; if (Checked) { return; } Checked = true; if (Core::App().settings().nativeNotifications() && !Platform::IsWayland()) { ComputeSupported(true); } else { ComputeSupported(); } } std::vector ComputeServerInformation() { std::vector serverInformation; const auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("GetServerInformation")); const auto reply = QDBusConnection::sessionBus().call(message); if (reply.type() == QDBusMessage::ReplyMessage) { ranges::transform( reply.arguments(), ranges::back_inserter(serverInformation), &QVariant::toString ); } else if (reply.type() == QDBusMessage::ErrorMessage) { LOG(("Native notification error: %1").arg(reply.errorMessage())); } else { LOG(("Native notification error: " "invalid reply from GetServerInformation")); } return serverInformation; } std::vector GetServerInformation() { static const auto Result = ComputeServerInformation(); return Result; } QStringList ComputeCapabilities() { const auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("GetCapabilities")); const QDBusReply reply = QDBusConnection::sessionBus().call( message); if (reply.isValid()) { return reply.value(); } else { LOG(("Native notification error: %1").arg(reply.error().message())); } return {}; } QStringList GetCapabilities() { static const auto Result = ComputeCapabilities(); return Result; } bool Inhibited() { auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kPropertiesInterface.utf16(), qsl("Get")); message.setArguments({ qsl("org.freedesktop.Notifications"), qsl("Inhibited") }); const QDBusReply reply = QDBusConnection::sessionBus().call( message); static const auto NotSupportedErrors = { QDBusError::ServiceUnknown, QDBusError::InvalidArgs, }; if (reply.isValid()) { return reply.value().toBool(); } else if (ranges::contains(NotSupportedErrors, reply.error().type())) { InhibitedNotSupported = true; } else { if (reply.error().type() == QDBusError::AccessDenied) { InhibitedNotSupported = true; } LOG(("Native notification error: %1").arg(reply.error().message())); } return false; } QVersionNumber ParseSpecificationVersion( const std::vector &serverInformation) { if (serverInformation.size() >= 4) { return QVersionNumber::fromString(serverInformation[3]); } else { LOG(("Native notification error: " "server information should have 4 elements")); } return QVersionNumber(); } QString GetImageKey(const QVersionNumber &specificationVersion) { if (!specificationVersion.isNull()) { if (specificationVersion >= QVersionNumber(1, 2)) { return qsl("image-data"); } else if (specificationVersion == QVersionNumber(1, 1)) { return qsl("image_data"); } else if (specificationVersion < QVersionNumber(1, 1)) { return qsl("icon_data"); } else { LOG(("Native notification error: unknown specification version")); } } else { LOG(("Native notification error: specification version is null")); } return QString(); } } // namespace NotificationData::NotificationData( const base::weak_ptr &manager, const QString &title, const QString &subtitle, const QString &msg, NotificationId id, bool hideReplyButton) : _dbusConnection(QDBusConnection::sessionBus()) , _manager(manager) , _title(title) , _imageKey(GetImageKey(ParseSpecificationVersion( GetServerInformation()))) , _id(id) { const auto capabilities = GetCapabilities(); if (capabilities.contains(qsl("body-markup"))) { _body = subtitle.isEmpty() ? msg.toHtmlEscaped() : qsl("%1\n%2") .arg(subtitle.toHtmlEscaped()) .arg(msg.toHtmlEscaped()); } else { _body = subtitle.isEmpty() ? msg : qsl("%1\n%2").arg(subtitle).arg(msg); } if (capabilities.contains(qsl("actions"))) { _actions << qsl("default") << QString(); _actions << qsl("mail-mark-read") << tr::lng_context_mark_read(tr::now); if (capabilities.contains(qsl("inline-reply")) && !hideReplyButton) { _actions << qsl("inline-reply") << tr::lng_notification_reply(tr::now); _dbusConnection.connect( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("NotificationReplied"), this, SLOT(notificationReplied(uint,QString))); } else { // icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html _actions << qsl("mail-reply-sender") << tr::lng_notification_reply(tr::now); } _dbusConnection.connect( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("ActionInvoked"), this, SLOT(actionInvoked(uint,QString))); } if (capabilities.contains(qsl("action-icons"))) { _hints["action-icons"] = true; } // suppress system sound if telegram sound activated, otherwise use system sound if (capabilities.contains(qsl("sound"))) { if (Core::App().settings().soundNotify()) { _hints["suppress-sound"] = true; } else { // sound name according to http://0pointer.de/public/sound-naming-spec.html _hints["sound-name"] = qsl("message-new-instant"); } } if (capabilities.contains(qsl("x-canonical-append"))) { _hints["x-canonical-append"] = qsl("true"); } _hints["category"] = qsl("im.received"); _hints["desktop-entry"] = GetLauncherBasename(); _dbusConnection.connect( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("NotificationClosed"), this, SLOT(notificationClosed(uint))); } bool NotificationData::show() { const auto iconName = _imageKey.isEmpty() || !_hints.contains(_imageKey) ? GetIconName() : QString(); auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("Notify")); message.setArguments({ AppName.utf16(), uint(0), iconName, _title, _body, _actions, _hints, -1 }); const QDBusReply reply = _dbusConnection.call( message); if (reply.isValid()) { _notificationId = reply.value(); } else { LOG(("Native notification error: %1").arg(reply.error().message())); } return reply.isValid(); } void NotificationData::close() { auto message = QDBusMessage::createMethodCall( kService.utf16(), kObjectPath.utf16(), kInterface.utf16(), qsl("CloseNotification")); message.setArguments({ _notificationId }); _dbusConnection.send(message); } void NotificationData::setImage(const QString &imagePath) { if (_imageKey.isEmpty()) { return; } const auto image = QImage(imagePath) .convertToFormat(QImage::Format_RGBA8888); const QByteArray imageBytes( (const char*)image.constBits(), #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) image.byteCount()); #else // Qt < 5.10.0 image.sizeInBytes()); #endif // Qt >= 5.10.0 const auto imageData = ImageData{ image.width(), image.height(), image.bytesPerLine(), true, 8, 4, imageBytes }; _hints[_imageKey] = QVariant::fromValue(imageData); } void NotificationData::notificationClosed(uint id) { if (id == _notificationId) { const auto manager = _manager; const auto my = _id; crl::on_main(manager, [=] { manager->clearNotification(my); }); } } void NotificationData::actionInvoked(uint id, const QString &actionName) { if (id != _notificationId) { return; } if (actionName == qsl("default") || actionName == qsl("mail-reply-sender")) { const auto manager = _manager; const auto my = _id; crl::on_main(manager, [=] { manager->notificationActivated(my); }); } else if (actionName == qsl("mail-mark-read")) { const auto manager = _manager; const auto my = _id; crl::on_main(manager, [=] { manager->notificationReplied(my, {}); }); } } void NotificationData::notificationReplied(uint id, const QString &text) { if (id == _notificationId) { const auto manager = _manager; const auto my = _id; crl::on_main(manager, [=] { manager->notificationReplied(my, { text, {} }); }); } } QDBusArgument &operator<<( QDBusArgument &argument, const NotificationData::ImageData &imageData) { argument.beginStructure(); argument << imageData.width << imageData.height << imageData.rowStride << imageData.hasAlpha << imageData.bitsPerSample << imageData.channels << imageData.data; argument.endStructure(); return argument; } const QDBusArgument &operator>>( const QDBusArgument &argument, NotificationData::ImageData &imageData) { argument.beginStructure(); argument >> imageData.width >> imageData.height >> imageData.rowStride >> imageData.hasAlpha >> imageData.bitsPerSample >> imageData.channels >> imageData.data; argument.endStructure(); return argument; } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION bool SkipAudio() { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION if (Supported() && GetCapabilities().contains(qsl("inhibitions")) && !InhibitedNotSupported) { return Inhibited(); } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION return false; } bool SkipToast() { return SkipAudio(); } bool SkipFlashBounce() { return SkipAudio(); } bool Supported() { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION return NotificationsSupported; #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION return false; } std::unique_ptr Create( Window::Notifications::System *system) { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION GetSupported(); if ((Core::App().settings().nativeNotifications() && Supported()) || Platform::IsWayland()) { return std::make_unique(system); } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION return nullptr; } #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION Manager::Private::Private(not_null manager, Type type) : _cachedUserpics(type) , _manager(manager) { qDBusRegisterMetaType(); if (!Supported()) { return; } const auto serverInformation = GetServerInformation(); const auto capabilities = GetCapabilities(); if (!serverInformation.empty()) { LOG(("Notification daemon product name: %1") .arg(serverInformation[0])); LOG(("Notification daemon vendor name: %1") .arg(serverInformation[1])); LOG(("Notification daemon version: %1") .arg(serverInformation[2])); LOG(("Notification daemon specification version: %1") .arg(serverInformation[3])); } if (!capabilities.isEmpty()) { LOG(("Notification daemon capabilities: %1") .arg(capabilities.join(", "))); } } void Manager::Private::showNotification( not_null peer, std::shared_ptr &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, bool hideNameAndPhoto, bool hideReplyButton) { if (!Supported()) return; const auto key = FullPeer{ .sessionId = peer->session().uniqueId(), .peerId = peer->id }; auto notification = std::make_shared( _manager, title, subtitle, msg, NotificationId{ .full = key, .msgId = msgId }, hideReplyButton); if (!hideNameAndPhoto) { const auto userpicKey = peer->userpicUniqueKey(userpicView); notification->setImage( _cachedUserpics.get(userpicKey, peer, userpicView)); } auto i = _notifications.find(key); if (i != _notifications.cend()) { auto j = i->second.find(msgId); if (j != i->second.end()) { auto oldNotification = j->second; i->second.erase(j); oldNotification->close(); i = _notifications.find(key); } } if (i == _notifications.cend()) { i = _notifications.emplace( key, base::flat_map()).first; } i->second.emplace(msgId, notification); if (!notification->show()) { i = _notifications.find(key); if (i != _notifications.cend()) { i->second.remove(msgId); if (i->second.empty()) { _notifications.erase(i); } } } } void Manager::Private::clearAll() { if (!Supported()) return; for (const auto &[key, notifications] : base::take(_notifications)) { for (const auto &[msgId, notification] : notifications) { notification->close(); } } } void Manager::Private::clearFromHistory(not_null history) { if (!Supported()) return; const auto key = FullPeer{ .sessionId = history->session().uniqueId(), .peerId = history->peer->id }; auto i = _notifications.find(key); if (i != _notifications.cend()) { const auto temp = base::take(i->second); _notifications.erase(i); for (const auto &[msgId, notification] : temp) { notification->close(); } } } void Manager::Private::clearFromSession(not_null session) { if (!Supported()) return; const auto sessionId = session->uniqueId(); for (auto i = _notifications.begin(); i != _notifications.end();) { if (i->first.sessionId == sessionId) { const auto temp = base::take(i->second); i = _notifications.erase(i); for (const auto &[msgId, notification] : temp) { notification->close(); } } } } void Manager::Private::clearNotification(NotificationId id) { if (!Supported()) return; auto i = _notifications.find(id.full); if (i != _notifications.cend()) { if (i->second.remove(id.msgId) && i->second.empty()) { _notifications.erase(i); } } } Manager::Private::~Private() { clearAll(); } Manager::Manager(not_null system) : NativeManager(system) , _private(std::make_unique(this, Private::Type::Rounded)) { } void Manager::clearNotification(NotificationId id) { _private->clearNotification(id); } Manager::~Manager() = default; void Manager::doShowNativeNotification( not_null peer, std::shared_ptr &userpicView, MsgId msgId, const QString &title, const QString &subtitle, const QString &msg, bool hideNameAndPhoto, bool hideReplyButton) { _private->showNotification( peer, userpicView, msgId, title, subtitle, msg, hideNameAndPhoto, hideReplyButton); } void Manager::doClearAllFast() { _private->clearAll(); } void Manager::doClearFromHistory(not_null history) { _private->clearFromHistory(history); } void Manager::doClearFromSession(not_null session) { _private->clearFromSession(session); } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION } // namespace Notifications } // namespace Platform