mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-14 02:39:06 +02:00
The daemon publishes the offer at INFO severity, so with notifications off the tray dropped it. An offer is a consent request that expires: unseen, the transfer is silently lost. Pass it like a critical event, leaving the file drop events that only inform behind the toggle.
149 lines
4.8 KiB
Go
149 lines
4.8 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
|
|
}
|
|
// 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)
|
|
// A file drop offer is a consent request that expires, not a report of
|
|
// something that already happened: unseen, it takes the transfer with it.
|
|
// It passes the gate like a critical event, while the file drop events that
|
|
// only inform (completed, failed, withdrawn) stay behind it.
|
|
isFileDropOffer := se.Metadata[proto.MetadataKindKey] == proto.MetadataKindFileDropOffer
|
|
t.profileMu.Lock()
|
|
enabled := t.notificationsEnabled
|
|
t.profileMu.Unlock()
|
|
if !enabled && !critical && !isFileDropOffer {
|
|
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 isFileDropOffer {
|
|
t.notifyFileDropOffer(se)
|
|
return
|
|
}
|
|
|
|
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" {
|
|
deadline, _ := authsession.ParseExpiresAt(se.Metadata[authsession.MetaExpiresAt])
|
|
t.openSessionExpiration(deadline)
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
}
|