rss email digests over ssh because you're a cool kid herald.dunkirk.sh
go rss rss-reader ssh charm

feat: mark new rss feeds as seen

dunkirk.sh 95f9f1ff 5464eda1

verified
+57 -8
+2 -2
scheduler/scheduler.go
··· 258 258 259 259 s.logger.Debug("RunNow: calculating next run") 260 260 now := time.Now().UTC() 261 - nextRun, err := gronx.NextTick(cfg.CronExpr, true) 261 + nextRun, err := gronx.NextTickAfter(cfg.CronExpr, now, true) 262 262 if err != nil { 263 263 return stats, fmt.Errorf("calculate next run: %w", err) 264 264 } ··· 487 487 } 488 488 489 489 now := time.Now().UTC() 490 - nextRun, err := gronx.NextTick(cfg.CronExpr, true) 490 + nextRun, err := gronx.NextTickAfter(cfg.CronExpr, now, true) 491 491 if err != nil { 492 492 return fmt.Errorf("calculate next run: %w", err) 493 493 }
+28 -3
ssh/scp.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + "context" 6 + "database/sql" 5 7 "fmt" 6 8 "io" 7 9 "io/fs" ··· 197 199 return 0, fmt.Errorf("failed to update feed: %w", err) 198 200 } 199 201 } else { 200 - // New feed - create it 201 - if _, err := h.store.CreateFeedTx(ctx, tx, cfg.ID, newFeed.URL, newFeed.Name); err != nil { 202 + // New feed - create it and mark existing items as seen 203 + newFeedRecord, err := h.store.CreateFeedTx(ctx, tx, cfg.ID, newFeed.URL, newFeed.Name) 204 + if err != nil { 202 205 return 0, fmt.Errorf("failed to create feed: %w", err) 206 + } 207 + // Pre-seed seen items so we don't send old posts 208 + if err := h.preseedSeenItems(ctx, tx, newFeedRecord); err != nil { 209 + h.logger.Warn("failed to preseed seen items", "feed_url", newFeed.URL, "err", err) 203 210 } 204 211 } 205 212 } ··· 239 246 } 240 247 241 248 func calculateNextRun(cronExpr string) (time.Time, error) { 242 - return gronx.NextTick(cronExpr, true) 249 + return gronx.NextTickAfter(cronExpr, time.Now().UTC(), true) 243 250 } 244 251 245 252 type configFileInfo struct { ··· 261 268 func (e *configDirEntry) IsDir() bool { return false } 262 269 func (e *configDirEntry) Type() fs.FileMode { return e.info.Mode() } 263 270 func (e *configDirEntry) Info() (fs.FileInfo, error) { return e.info, nil } 271 + 272 + // preseedSeenItems fetches the feed and marks all current items as seen, 273 + // so that adding a new feed doesn't trigger emails for old posts. 274 + func (h *scpHandler) preseedSeenItems(ctx context.Context, tx *sql.Tx, feed *store.Feed) error { 275 + result := scheduler.FetchFeed(ctx, feed) 276 + if result.Error != nil { 277 + return result.Error 278 + } 279 + 280 + for _, item := range result.Items { 281 + if err := h.store.MarkItemSeenTx(ctx, tx, feed.ID, item.GUID, item.Title, item.Link); err != nil { 282 + return err 283 + } 284 + } 285 + 286 + h.logger.Debug("preseeded seen items for new feed", "feed_url", feed.URL, "count", len(result.Items)) 287 + return nil 288 + }
+26 -2
ssh/sftp.go
··· 1 1 package ssh 2 2 3 3 import ( 4 + "context" 4 5 "fmt" 5 6 "io" 6 7 "io/fs" ··· 218 219 return fmt.Errorf("failed to update feed: %w", err) 219 220 } 220 221 } else { 221 - // New feed - create it 222 - if _, err := w.handler.store.CreateFeed(ctx, cfg.ID, newFeed.URL, newFeed.Name); err != nil { 222 + // New feed - create it and mark existing items as seen 223 + newFeedRecord, err := w.handler.store.CreateFeed(ctx, cfg.ID, newFeed.URL, newFeed.Name) 224 + if err != nil { 223 225 return fmt.Errorf("failed to create feed: %w", err) 226 + } 227 + // Pre-seed seen items so we don't send old posts 228 + if err := w.preseedSeenItems(ctx, newFeedRecord); err != nil { 229 + w.handler.logger.Warn("failed to preseed seen items", "feed_url", newFeed.URL, "err", err) 224 230 } 225 231 } 226 232 } ··· 252 258 } 253 259 254 260 w.handler.logger.Info("config uploaded via SFTP", "user_id", w.handler.user.ID, "filename", w.filename, "feeds", len(parsed.Feeds)) 261 + return nil 262 + } 263 + 264 + // preseedSeenItems fetches the feed and marks all current items as seen, 265 + // so that adding a new feed doesn't trigger emails for old posts. 266 + func (w *configWriter) preseedSeenItems(ctx context.Context, feed *store.Feed) error { 267 + result := scheduler.FetchFeed(ctx, feed) 268 + if result.Error != nil { 269 + return result.Error 270 + } 271 + 272 + for _, item := range result.Items { 273 + if err := w.handler.store.MarkItemSeen(ctx, feed.ID, item.GUID, item.Title, item.Link); err != nil { 274 + return err 275 + } 276 + } 277 + 278 + w.handler.logger.Debug("preseeded seen items for new feed", "feed_url", feed.URL, "count", len(result.Items)) 255 279 return nil 256 280 } 257 281
+1 -1
store/configs.go
··· 254 254 return err 255 255 } 256 256 257 - nextRun, err := gronx.NextTick(cfg.CronExpr, true) 257 + nextRun, err := gronx.NextTickAfter(cfg.CronExpr, time.Now().UTC(), true) 258 258 if err != nil { 259 259 return fmt.Errorf("calculate next run: %w", err) 260 260 }