tangled
alpha
login
or
join now
dunkirk.sh
/
herald
1
fork
atom
rss email digests over ssh because you're a cool kid
herald.dunkirk.sh
go
rss
rss-reader
ssh
charm
1
fork
atom
overview
issues
pulls
pipelines
feat: mark new rss feeds as seen
dunkirk.sh
2 months ago
95f9f1ff
5464eda1
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+57
-8
4 changed files
expand all
collapse all
unified
split
scheduler
scheduler.go
ssh
scp.go
sftp.go
store
configs.go
+2
-2
scheduler/scheduler.go
···
258
258
259
259
s.logger.Debug("RunNow: calculating next run")
260
260
now := time.Now().UTC()
261
261
-
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
261
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
490
-
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
490
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
5
+
"context"
6
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
200
-
// New feed - create it
201
201
-
if _, err := h.store.CreateFeedTx(ctx, tx, cfg.ID, newFeed.URL, newFeed.Name); err != nil {
202
202
+
// New feed - create it and mark existing items as seen
203
203
+
newFeedRecord, err := h.store.CreateFeedTx(ctx, tx, cfg.ID, newFeed.URL, newFeed.Name)
204
204
+
if err != nil {
202
205
return 0, fmt.Errorf("failed to create feed: %w", err)
206
206
+
}
207
207
+
// Pre-seed seen items so we don't send old posts
208
208
+
if err := h.preseedSeenItems(ctx, tx, newFeedRecord); err != nil {
209
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
242
-
return gronx.NextTick(cronExpr, true)
249
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
271
+
272
272
+
// preseedSeenItems fetches the feed and marks all current items as seen,
273
273
+
// so that adding a new feed doesn't trigger emails for old posts.
274
274
+
func (h *scpHandler) preseedSeenItems(ctx context.Context, tx *sql.Tx, feed *store.Feed) error {
275
275
+
result := scheduler.FetchFeed(ctx, feed)
276
276
+
if result.Error != nil {
277
277
+
return result.Error
278
278
+
}
279
279
+
280
280
+
for _, item := range result.Items {
281
281
+
if err := h.store.MarkItemSeenTx(ctx, tx, feed.ID, item.GUID, item.Title, item.Link); err != nil {
282
282
+
return err
283
283
+
}
284
284
+
}
285
285
+
286
286
+
h.logger.Debug("preseeded seen items for new feed", "feed_url", feed.URL, "count", len(result.Items))
287
287
+
return nil
288
288
+
}
+26
-2
ssh/sftp.go
···
1
1
package ssh
2
2
3
3
import (
4
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
221
-
// New feed - create it
222
222
-
if _, err := w.handler.store.CreateFeed(ctx, cfg.ID, newFeed.URL, newFeed.Name); err != nil {
222
222
+
// New feed - create it and mark existing items as seen
223
223
+
newFeedRecord, err := w.handler.store.CreateFeed(ctx, cfg.ID, newFeed.URL, newFeed.Name)
224
224
+
if err != nil {
223
225
return fmt.Errorf("failed to create feed: %w", err)
226
226
+
}
227
227
+
// Pre-seed seen items so we don't send old posts
228
228
+
if err := w.preseedSeenItems(ctx, newFeedRecord); err != nil {
229
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
261
+
return nil
262
262
+
}
263
263
+
264
264
+
// preseedSeenItems fetches the feed and marks all current items as seen,
265
265
+
// so that adding a new feed doesn't trigger emails for old posts.
266
266
+
func (w *configWriter) preseedSeenItems(ctx context.Context, feed *store.Feed) error {
267
267
+
result := scheduler.FetchFeed(ctx, feed)
268
268
+
if result.Error != nil {
269
269
+
return result.Error
270
270
+
}
271
271
+
272
272
+
for _, item := range result.Items {
273
273
+
if err := w.handler.store.MarkItemSeen(ctx, feed.ID, item.GUID, item.Title, item.Link); err != nil {
274
274
+
return err
275
275
+
}
276
276
+
}
277
277
+
278
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
257
-
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
257
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
}