rss email digests over ssh because you're a cool kid
herald.dunkirk.sh
go
rss
rss-reader
ssh
charm
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}