package notify import ( "bytes" "fmt" "net" "net/http" "net/mail" "net/smtp" "strings" "time" "ncore-hnr/internal/model" ) const manualNeededSubject = "nCore HnR manual work" type Sender interface { SendManualNeeded(results []model.ActionResult) error } type NotificationNTFY struct { URL string HTTPClient *http.Client } func (n NotificationNTFY) SendManualNeeded(results []model.ActionResult) error { body, ok := manualNeededText(results) if strings.TrimSpace(n.URL) == "" || !ok { return nil } client := n.HTTPClient if client == nil { client = &http.Client{Timeout: 15 * time.Second} } req, err := http.NewRequest(http.MethodPost, n.URL, bytes.NewBufferString(body)) if err != nil { return err } req.Header.Set("Content-Type", "text/plain; charset=utf-8") req.Header.Set("Title", manualNeededSubject) resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("notify returned %s", resp.Status) } return nil } type NotificationSMTP struct { Host string Port string Username string Password string From string To string } func (s NotificationSMTP) SendManualNeeded(results []model.ActionResult) error { body, ok := manualNeededText(results) if strings.TrimSpace(s.Host) == "" || !ok { return nil } host := strings.TrimSpace(s.Host) addr := net.JoinHostPort(host, strings.TrimSpace(s.Port)) from, err := mail.ParseAddress(strings.TrimSpace(s.From)) if err != nil { return fmt.Errorf("parse smtp from: %w", err) } recipients, err := mail.ParseAddressList(strings.TrimSpace(s.To)) if err != nil { return fmt.Errorf("parse smtp to: %w", err) } to := make([]string, 0, len(recipients)) for _, recipient := range recipients { to = append(to, recipient.Address) } var auth smtp.Auth if strings.TrimSpace(s.Username) != "" || strings.TrimSpace(s.Password) != "" { auth = smtp.PlainAuth("", strings.TrimSpace(s.Username), strings.TrimSpace(s.Password), host) } message := strings.Builder{} message.WriteString(fmt.Sprintf("From: %s\r\n", from.String())) message.WriteString(fmt.Sprintf("To: %s\r\n", formatAddressList(recipients))) message.WriteString(fmt.Sprintf("Subject: %s\r\n", manualNeededSubject)) message.WriteString("MIME-Version: 1.0\r\n") message.WriteString("Content-Type: text/plain; charset=UTF-8\r\n") message.WriteString("\r\n") message.WriteString(body) if err := smtp.SendMail(addr, auth, from.Address, to, []byte(message.String())); err != nil { return fmt.Errorf("send smtp notification: %w", err) } return nil } func manualNeededText(results []model.ActionResult) (string, bool) { var body strings.Builder manualCount := 0 body.WriteString("nCore HnR torrents need manual work:\n") for _, result := range results { if !result.ManualNeeded { continue } manualCount++ body.WriteString(fmt.Sprintf("- %s", result.Torrent.Name)) if result.QBit != nil { body.WriteString(fmt.Sprintf(" (qBit: %s, %.1f%%)", result.QBit.State, result.QBit.Progress*100)) } body.WriteString("\n") } return body.String(), manualCount > 0 } func formatAddressList(addresses []*mail.Address) string { formatted := make([]string, 0, len(addresses)) for _, address := range addresses { formatted = append(formatted, address.String()) } return strings.Join(formatted, ", ") }