rss email digests over ssh because you're a cool kid herald.dunkirk.sh
go rss rss-reader ssh charm
at main 91 lines 2.0 kB view raw
1package config 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/mail" 9 "net/url" 10 "time" 11 12 "github.com/adhocore/gronx" 13 "github.com/mmcdole/gofeed" 14) 15 16var ( 17 ErrNoEmail = errors.New("email is required") 18 ErrBadEmail = errors.New("invalid email format") 19 ErrNoCron = errors.New("cron expression is required") 20 ErrBadCron = errors.New("invalid cron expression") 21 ErrNoFeeds = errors.New("at least one feed URL is required") 22 ErrBadFeedURL = errors.New("invalid feed URL") 23) 24 25func Validate(cfg *ParsedConfig) error { 26 if cfg.Email == "" { 27 return ErrNoEmail 28 } 29 if _, err := mail.ParseAddress(cfg.Email); err != nil { 30 return ErrBadEmail 31 } 32 33 if cfg.CronExpr == "" { 34 return ErrNoCron 35 } 36 gron := gronx.New() 37 if !gron.IsValid(cfg.CronExpr) { 38 return ErrBadCron 39 } 40 41 if len(cfg.Feeds) == 0 { 42 return ErrNoFeeds 43 } 44 45 for _, feed := range cfg.Feeds { 46 u, err := url.Parse(feed.URL) 47 if err != nil || u.Scheme == "" || u.Host == "" { 48 return ErrBadFeedURL 49 } 50 } 51 52 return nil 53} 54 55// ValidateFeedURLs attempts to fetch and parse each feed URL with a short timeout 56func ValidateFeedURLs(ctx context.Context, cfg *ParsedConfig) error { 57 ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 58 defer cancel() 59 60 parser := gofeed.NewParser() 61 client := &http.Client{ 62 Timeout: 5 * time.Second, 63 } 64 65 for _, feed := range cfg.Feeds { 66 req, err := http.NewRequestWithContext(ctx, http.MethodGet, feed.URL, nil) 67 if err != nil { 68 return fmt.Errorf("invalid feed URL %s: %w", feed.URL, err) 69 } 70 71 req.Header.Set("User-Agent", "Herald/1.0 (RSS Aggregator)") 72 73 resp, err := client.Do(req) 74 if err != nil { 75 return fmt.Errorf("failed to fetch feed %s: %w", feed.URL, err) 76 } 77 78 if resp.StatusCode != http.StatusOK { 79 _ = resp.Body.Close() 80 return fmt.Errorf("feed %s returned status %d", feed.URL, resp.StatusCode) 81 } 82 83 _, err = parser.Parse(resp.Body) 84 _ = resp.Body.Close() 85 if err != nil { 86 return fmt.Errorf("failed to parse feed %s: %w", feed.URL, err) 87 } 88 } 89 90 return nil 91}