diff --git a/doc/19-technical-concepts.md b/doc/19-technical-concepts.md
index 1da32b6f2..74a2756ca 100644
--- a/doc/19-technical-concepts.md
+++ b/doc/19-technical-concepts.md
@@ -1514,6 +1514,42 @@ Message updates will be dropped when:
* Notification does not exist.
* Origin endpoint's zone is not allowed to access this checkable.
+#### event::UpdateLastNotifiedStatePerUser
+
+> Location: `clusterevents.cpp`
+
+##### Message Body
+
+Key | Value
+----------|---------
+jsonrpc | 2.0
+method | event::UpdateLastNotifiedStatePerUser
+params | Dictionary
+
+##### Params
+
+Key | Type | Description
+-------------|--------|------------------
+notification | String | Notification name
+user | String | User name
+state | Number | Checkable state the user just got a problem notification for
+
+Used to sync the state of a notification object within the same HA zone.
+
+##### Functions
+
+Event Sender: `Notification::OnLastNotifiedStatePerUserUpdated`
+Event Receiver: `LastNotifiedStatePerUserUpdatedAPIHandler`
+
+##### Permissions
+
+The receiver will not process messages from not configured endpoints.
+
+Message updates will be dropped when:
+
+* Notification does not exist.
+* Origin endpoint is not within the local zone.
+
#### event::SetForceNextCheck
> Location: `clusterevents.cpp`
diff --git a/lib/icinga/clusterevents.cpp b/lib/icinga/clusterevents.cpp
index 6c4daa8b8..95616d2e3 100644
--- a/lib/icinga/clusterevents.cpp
+++ b/lib/icinga/clusterevents.cpp
@@ -29,6 +29,7 @@ REGISTER_APIFUNCTION(SetStateBeforeSuppression, event, &ClusterEvents::StateBefo
REGISTER_APIFUNCTION(SetSuppressedNotifications, event, &ClusterEvents::SuppressedNotificationsChangedAPIHandler);
REGISTER_APIFUNCTION(SetSuppressedNotificationTypes, event, &ClusterEvents::SuppressedNotificationTypesChangedAPIHandler);
REGISTER_APIFUNCTION(SetNextNotification, event, &ClusterEvents::NextNotificationChangedAPIHandler);
+REGISTER_APIFUNCTION(UpdateLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextCheck, event, &ClusterEvents::ForceNextCheckChangedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextNotification, event, &ClusterEvents::ForceNextNotificationChangedAPIHandler);
REGISTER_APIFUNCTION(SetAcknowledgement, event, &ClusterEvents::AcknowledgementSetAPIHandler);
@@ -50,6 +51,7 @@ void ClusterEvents::StaticInitialize()
Checkable::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationsChangedHandler);
Notification::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationTypesChangedHandler);
Notification::OnNextNotificationChanged.connect(&ClusterEvents::NextNotificationChangedHandler);
+ Notification::OnLastNotifiedStatePerUserUpdated.connect(&ClusterEvents::LastNotifiedStatePerUserUpdatedHandler);
Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
@@ -529,6 +531,64 @@ Value ClusterEvents::NextNotificationChangedAPIHandler(const MessageOrigin::Ptr&
return Empty;
}
+void ClusterEvents::LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+ params->Set("user", user);
+ params->Set("state", state);
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::UpdateLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user updated' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ auto state (params->Get("state"));
+
+ if (!state.IsNumber()) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Set(params->Get("user"), state);
+ Notification::OnLastNotifiedStatePerUserUpdated(notification, params->Get("user"), state, origin);
+
+ return Empty;
+}
+
void ClusterEvents::ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
{
ApiListener::Ptr listener = ApiListener::GetInstance();
diff --git a/lib/icinga/clusterevents.hpp b/lib/icinga/clusterevents.hpp
index 4cdadacc9..2e6e092f2 100644
--- a/lib/icinga/clusterevents.hpp
+++ b/lib/icinga/clusterevents.hpp
@@ -41,6 +41,9 @@ public:
static void NextNotificationChangedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
static Value NextNotificationChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static void LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
static void ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
static Value ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
diff --git a/lib/icinga/notification.cpp b/lib/icinga/notification.cpp
index 05628969c..8a254b8fc 100644
--- a/lib/icinga/notification.cpp
+++ b/lib/icinga/notification.cpp
@@ -23,6 +23,7 @@ std::map Notification::m_StateFilterMap;
std::map Notification::m_TypeFilterMap;
boost::signals2::signal Notification::OnNextNotificationChanged;
+boost::signals2::signal Notification::OnLastNotifiedStatePerUserUpdated;
String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
{
@@ -456,7 +457,12 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
case NotificationProblem:
case NotificationRecovery: {
auto [host, service] = GetHostService(checkable);
- GetLastNotifiedStatePerUser()->Set(userName, service ? service->GetState() : host->GetState());
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
+
+ if (state != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ GetLastNotifiedStatePerUser()->Set(userName, state);
+ OnLastNotifiedStatePerUserUpdated(this, userName, state, nullptr);
+ }
}
default:
;
diff --git a/lib/icinga/notification.hpp b/lib/icinga/notification.hpp
index ec9164f19..1c6a2f44d 100644
--- a/lib/icinga/notification.hpp
+++ b/lib/icinga/notification.hpp
@@ -13,6 +13,7 @@
#include "remote/endpoint.hpp"
#include "remote/messageorigin.hpp"
#include "base/array.hpp"
+#include
namespace icinga
{
@@ -92,6 +93,7 @@ public:
static String NotificationHostStateToString(HostState state);
static boost::signals2::signal OnNextNotificationChanged;
+ static boost::signals2::signal OnLastNotifiedStatePerUserUpdated;
void Validate(int types, const ValidationUtils& utils) override;