mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-28 10:31:29 +02:00
170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/ui/authsession"
|
|
"github.com/netbirdio/netbird/client/ui/services"
|
|
)
|
|
|
|
// onSystemEvent fires an OS notification for daemon SystemEvents that carry a
|
|
// user-facing message. Gated by the "Notifications" toggle; critical events bypass it.
|
|
func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
|
|
se, ok := ev.Data.(services.SystemEvent)
|
|
if !ok {
|
|
return
|
|
}
|
|
// config_changed carries no UserMessage, so handle it before the message gate below.
|
|
if se.Category == "system" && se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypeConfigChanged {
|
|
log.Infof("config_changed event received (source=%s); refreshing tray restrictions", se.Metadata[proto.MetadataSourceKey])
|
|
go t.refreshRestrictions()
|
|
go t.loadConfig()
|
|
// MDM gets a localised toast here; the daemon's English "policy_applied"
|
|
// event is suppressed in shouldSkipSystemEvent. Other sources stay silent.
|
|
if se.Metadata[proto.MetadataSourceKey] == proto.MetadataSourceMDM {
|
|
t.profileMu.Lock()
|
|
enabled := t.notificationsEnabled
|
|
t.profileMu.Unlock()
|
|
if enabled {
|
|
t.notify(
|
|
t.loc.T("notify.mdm.policyApplied.title"),
|
|
t.loc.T("notify.mdm.policyApplied.body"),
|
|
notifyIDMDMPolicy,
|
|
)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
// Inbound-connection approval prompts open a dedicated dialog instead of a
|
|
// toast. Handle before the message gate: the daemon auto-denies on its
|
|
// deadline, so a missing WindowManager fails closed.
|
|
if se.Category == "approval" {
|
|
t.openApproval(se)
|
|
return
|
|
}
|
|
|
|
// Session-warning and deadline-rejected events build their body locally from
|
|
// metadata; every other event needs a UserMessage.
|
|
isSessionWarning := se.Metadata[authsession.MetaWarning] == "true"
|
|
isDeadlineRejected := se.Metadata[authsession.MetaDeadlineRejected] != ""
|
|
if !isSessionWarning && !isDeadlineRejected && se.UserMessage == "" {
|
|
return
|
|
}
|
|
if shouldSkipSystemEvent(se) {
|
|
return
|
|
}
|
|
|
|
critical := strings.EqualFold(se.Severity, services.SeverityCritical)
|
|
t.profileMu.Lock()
|
|
enabled := t.notificationsEnabled
|
|
t.profileMu.Unlock()
|
|
if !enabled && !critical {
|
|
return
|
|
}
|
|
|
|
// Session-warning events route via stable metadata flags rather than
|
|
// category/severity so a daemon-side reword still lands here. Final warning
|
|
// auto-opens the SessionExpiration dialog with no notification (the dialog is
|
|
// the last-chance reminder; doubling up would be noise).
|
|
if isDeadlineRejected {
|
|
t.notify(
|
|
t.loc.T("notify.sessionDeadlineRejected.title"),
|
|
t.loc.T("notify.sessionDeadlineRejected.body"),
|
|
notifyIDSessionExpired,
|
|
)
|
|
return
|
|
}
|
|
|
|
if se.Metadata != nil && se.Metadata[authsession.MetaWarning] == "true" {
|
|
if se.Metadata[authsession.MetaFinal] == "true" {
|
|
t.openSessionExpiration()
|
|
return
|
|
}
|
|
t.notifySessionWarning(
|
|
t.loc.T("notify.sessionWarning.title"),
|
|
t.buildSessionWarningBody(se.Metadata),
|
|
)
|
|
return
|
|
}
|
|
|
|
body := se.UserMessage
|
|
if id := se.Metadata["id"]; id != "" {
|
|
body += fmt.Sprintf(" ID: %s", id)
|
|
}
|
|
t.notify(eventTitle(se), body, notifyIDEvent+se.ID)
|
|
}
|
|
|
|
// openApproval opens the inbound-connection approval dialog from an APPROVAL
|
|
// SystemEvent. request_id is daemon-issued; without it the prompt can't be
|
|
// answered, so it's dropped and the daemon auto-denies on its deadline.
|
|
func (t *Tray) openApproval(se services.SystemEvent) {
|
|
if t.svc.WindowManager == nil {
|
|
return
|
|
}
|
|
requestID := se.Metadata["request_id"]
|
|
if requestID == "" {
|
|
log.Warnf("approval event missing request_id: %v", se.Metadata)
|
|
return
|
|
}
|
|
t.svc.WindowManager.OpenApproval(services.ApprovalRequest{
|
|
RequestID: requestID,
|
|
Kind: se.Metadata["kind"],
|
|
Initiator: se.Metadata["initiator"],
|
|
PeerName: se.Metadata["peer_name"],
|
|
SourceIP: se.Metadata["source_ip"],
|
|
Username: se.Metadata["username"],
|
|
PeerPubKey: se.Metadata["peer_pubkey"],
|
|
ExpiresAt: se.Metadata["expires_at"],
|
|
})
|
|
}
|
|
|
|
// eventTitle composes a notification title, e.g. "Critical: DNS", "Warning: Authentication".
|
|
func eventTitle(e services.SystemEvent) string {
|
|
prefix := titleCase(e.Severity)
|
|
if prefix == "" {
|
|
prefix = "Info"
|
|
}
|
|
category := titleCase(e.Category)
|
|
if category == "" {
|
|
category = "System"
|
|
}
|
|
return prefix + ": " + category
|
|
}
|
|
|
|
func titleCase(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
|
}
|
|
|
|
// shouldSkipSystemEvent reports whether a daemon SystemEvent must not surface as
|
|
// a tray notification:
|
|
// - update-available announcements (trayUpdater emits its own)
|
|
// - install-progress signals (consumed by the install-progress window)
|
|
// - the ::/0 partner of an exit-node default route (0.0.0.0/0 already toasted)
|
|
func shouldSkipSystemEvent(se services.SystemEvent) bool {
|
|
// "policy_applied" carries a hardcoded English message; the localised toast
|
|
// fires on the paired config_changed (source=mdm) event instead.
|
|
if se.Metadata[proto.MetadataTypeKey] == proto.MetadataTypePolicyApplied {
|
|
return true
|
|
}
|
|
if _, isUpdate := se.Metadata["new_version_available"]; isUpdate {
|
|
return true
|
|
}
|
|
if _, isProgress := se.Metadata["progress_window"]; isProgress {
|
|
return true
|
|
}
|
|
if se.Category == "network" && se.Metadata["network"] == "::/0" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|