diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cebffb7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.send.nrw/sendnrw/pcinfoagent + +go 1.25.3 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e329d8c --- /dev/null +++ b/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "os/user" + "strings" + "syscall" + "time" +) + +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 := fmt.Sprintf(` +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null +[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null + +# AppId von "Windows PowerShell" holen (oder erste gefundene App als Fallback) +$appid = (Get-StartApps | Where-Object Name -eq 'Windows PowerShell').AppId +if (-not $appid) { + $appid = (Get-StartApps | Select-Object -First 1).AppId +} + +$xmlString = @" + + + + %s + %s + + + +"@ + +$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) +`, title, message) + + 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 +}