package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "os/exec" "os/signal" "os/user" "strings" "syscall" "time" ) const appUserModelID = "de.stadthilden.GoSysNotifyAgent" type Notification struct { ID int64 `json:"id"` CreatedAt time.Time `json:"created_at"` Title string `json:"title"` Message string `json:"message"` TargetUser string `json:"target_user"` } var ( serviceURL = "http://127.0.0.1:24000" pollInterval = 10 * time.Second ) func main() { if v := os.Getenv("SERVICE_URL"); v != "" { serviceURL = strings.TrimRight(v, "/") } // Benutzername bestimmen (z. B. "DOMAIN\\user" oder "user") userName := currentUserName() log.Printf("Notification-Agent gestartet für Benutzer: %s", userName) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() var lastSeenID int64 = 0 ticker := time.NewTicker(pollInterval) defer ticker.Stop() // beim Start einmal sofort if err := pollOnce(ctx, userName, &lastSeenID); err != nil { log.Printf("initial poll error: %v", err) } for { select { case <-ctx.Done(): log.Println("Agent beendet.") return case <-ticker.C: if err := pollOnce(ctx, userName, &lastSeenID); err != nil { log.Printf("poll error: %v", err) } } } } func currentUserName() string { u, err := user.Current() if err == nil && u.Username != "" { return u.Username } if u := os.Getenv("USERNAME"); u != "" { return u } return "unknown" } func pollOnce(ctx context.Context, userName string, lastSeenID *int64) error { url := fmt.Sprintf("%s/api/notifications?user=%s&since_id=%d", serviceURL, urlQueryEscape(userName), *lastSeenID, ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("status %s", resp.Status) } var notifs []Notification if err := json.NewDecoder(resp.Body).Decode(¬ifs); err != nil { return err } var maxID = *lastSeenID for _, n := range notifs { if n.ID > maxID { maxID = n.ID } showErr := showToast(n.Title, n.Message) if showErr != nil { log.Printf("showToast error: %v", showErr) } } if maxID > *lastSeenID { *lastSeenID = maxID } return nil } func urlQueryEscape(s string) string { // minimal, reicht hier return strings.ReplaceAll(s, " ", "%20") } // === Toast === func escapeXML(s string) string { replacer := strings.NewReplacer( `&`, "&", `<`, "<", `>`, ">", `"`, """, `'`, "'", ) return replacer.Replace(s) } func showToast(title, message string) error { title = strings.TrimSpace(title) message = strings.TrimSpace(message) if title == "" { title = "Benachrichtigung" } if message == "" { return nil } // XML escapen title = escapeXML(title) message = escapeXML(message) psScript := ` [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null # bevorzugt Windows Terminal, sonst Windows PowerShell, sonst erste App $appid = (Get-StartApps | Where-Object Name -eq 'Windows Terminal').AppId if (-not $appid) { $appid = (Get-StartApps | Where-Object Name -eq 'Windows PowerShell').AppId } if (-not $appid) { $appid = (Get-StartApps | Select-Object -First 1).AppId } $xmlString = @" ` + title + ` ` + message + ` "@ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument $xml.LoadXml($xmlString) $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appid) $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) $notifier.Show($toast) ` cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("powershell toast error: %v, output: %s", err, string(out)) } return nil }