package main import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "os" "regexp" "strings" "time" ) // Konfiguration aus Umgebungsvariablen var ( gotosocialURL = os.Getenv("GOTOSOCIAL_URL") accessToken = os.Getenv("GOTOSOCIAL_TOKEN") ntfyServer = os.Getenv("NTFY_SERVER") ntfyToken = os.Getenv("NTFY_TOKEN") ntfyTopic = os.Getenv("NTFY_TOPIC") pollInterval, _ = time.ParseDuration(os.Getenv("POLL_INTERVAL")) ) type Notification struct { ID string `json:"id"` Type string `json:"type"` CreatedAt string `json:"created_at"` Account struct { DisplayName string `json:"display_name"` Acct string `json:"acct"` } `json:"account"` Status struct { ID string `json:"id"` Content string `json:"content"` URL string `json:"url"` // Original-URL des Posts } `json:"status"` } type NtfyAction struct { Action string `json:"action"` Label string `json:"label"` URL string `json:"url"` Clear bool `json:"clear"` } type NtfyMessage struct { Topic string `json:"topic"` Title string `json:"title"` Message string `json:"message"` Tags []string `json:"tags"` Priority int `json:"priority"` Actions []NtfyAction `json:"actions,omitempty"` } func main() { log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile) log.Println("Starte GoToSocial-Notifier...") if pollInterval == 0 { pollInterval = 30 * time.Second log.Printf("Kein POLL_INTERVAL gesetzt, verwende Default: %s", pollInterval) } if gotosocialURL == "" || accessToken == "" || ntfyServer == "" || ntfyTopic == "" { log.Fatal("Eine oder mehrere erforderliche Umgebungsvariablen fehlen") } ticker := time.NewTicker(pollInterval) defer ticker.Stop() lastID := "" for { select { case <-ticker.C: log.Println("Frage GoToSocial-Benachrichtigungen ab...") notifications, err := fetchNotifications(lastID) if err != nil { log.Printf("Fehler beim Abrufen: %v", err) continue } if len(notifications) > 0 { lastID = notifications[0].ID log.Printf("%d neue Benachrichtigungen gefunden", len(notifications)) } else { log.Println("Keine neuen Benachrichtigungen") } for _, n := range notifications { title := fmt.Sprintf("Neue Benachrichtigung von %s", n.Account.DisplayName) if err := sendToNtfy(n); err != nil { log.Printf("Fehler beim Senden von '%s': %v", title, err) } else { log.Printf("Benachrichtigung gesendet: '%s' (Typ: %s)", title, n.Type) } } } } } func fetchNotifications(sinceID string) ([]Notification, error) { req, err := http.NewRequestWithContext( context.Background(), "GET", gotosocialURL+"/api/v1/notifications", nil, ) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) q := req.URL.Query() q.Add("limit", "10") if sinceID != "" { q.Add("since_id", sinceID) } req.URL.RawQuery = q.Encode() resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("Statuscode: %d", resp.StatusCode) } var notifications []Notification if err := json.NewDecoder(resp.Body).Decode(¬ifications); err != nil { return nil, err } return notifications, nil } // HTML zu Plaintext konvertieren func htmlToPlaintext(html string) string { re := regexp.MustCompile(`<[^>]*>`) plain := re.ReplaceAllString(html, "") plain = strings.ReplaceAll(plain, "<", "<") plain = strings.ReplaceAll(plain, ">", ">") plain = strings.ReplaceAll(plain, "&", "&") plain = strings.ReplaceAll(plain, """, "\"") plain = strings.ReplaceAll(plain, "'", "'") return plain } func sendToNtfy(n Notification) error { plainContent := htmlToPlaintext(n.Status.Content) title := fmt.Sprintf("Neue Benachrichtigung von %s", n.Account.DisplayName) var actions []NtfyAction if n.Status.URL != "" { actions = []NtfyAction{ { Action: "view", Label: "Beitrag anzeigen", URL: n.Status.URL, Clear: true, }, } } msg := NtfyMessage{ Topic: ntfyTopic, Title: title, Message: fmt.Sprintf("Typ: %s\n\n%s", n.Type, plainContent), Tags: []string{"bell", "incoming_envelope"}, Priority: 4, Actions: actions, } jsonData, err := json.Marshal(msg) if err != nil { return err } req, err := http.NewRequest( "POST", ntfyServer, bytes.NewBuffer(jsonData), ) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if ntfyToken != "" { req.Header.Set("Authorization", "Bearer "+ntfyToken) } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ntfy antwortete mit: %d", resp.StatusCode) } return nil }