1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-06-27 16:36:00 +00:00

feat(filter): add EntryDate=max-age:duration filter

Example: `EntryDate=max-age:30d` or `EntryDate=max-age:1h`

Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d".
This commit is contained in:
Frédéric Guillot 2025-06-19 17:08:57 -07:00
parent b0a10f02fd
commit e6b814199b
2 changed files with 87 additions and 2 deletions

View file

@ -7,6 +7,7 @@ import (
"log/slog"
"regexp"
"slices"
"strconv"
"strings"
"time"
@ -183,6 +184,13 @@ func isDateMatchingPattern(pattern string, entryDate time.Time) bool {
return false
}
return entryDate.After(startDate) && entryDate.Before(endDate)
case "max-age":
duration, err := parseDuration(inputDate)
if err != nil {
return false
}
cutoffDate := time.Now().Add(-duration)
return entryDate.Before(cutoffDate)
}
return false
}
@ -195,3 +203,23 @@ func containsRegexPattern(pattern string, entries []string) bool {
}
return false
}
func parseDuration(duration string) (time.Duration, error) {
// Handle common duration formats like "30d", "7d", "1h", "1m", etc.
// Go's time.ParseDuration doesn't support days, so we handle them manually
if strings.HasSuffix(duration, "d") {
daysStr := strings.TrimSuffix(duration, "d")
days := 0
if daysStr != "" {
var err error
days, err = strconv.Atoi(daysStr)
if err != nil {
return 0, err
}
}
return time.Duration(days) * 24 * time.Hour, nil
}
// For other durations (hours, minutes, seconds), use Go's built-in parser
return time.ParseDuration(duration)
}

View file

@ -40,6 +40,9 @@ func TestBlockingEntries(t *testing.T) {
{&model.Feed{ID: 1}, &model.Entry{Author: "Different", Tags: []string{"example", "something else"}}, &model.User{BlockFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)Test"}, false},
{&model.Feed{ID: 1}, &model.Entry{Author: "Different", Tags: []string{"example", "test"}}, &model.User{BlockFilterEntryRules: "EntryAuthor\nEntryTag=(?i)Test"}, true},
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC)}, &model.User{BlockFilterEntryRules: "EntryDate=before:2024-03-15"}, true},
// Test max-age filter
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, &model.User{BlockFilterEntryRules: "EntryDate=max-age:30d"}, true}, // Entry from Jan 1, 2024 is definitely older than 30 days
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, &model.User{BlockFilterEntryRules: "EntryDate=max-age:invalid"}, false}, // Invalid duration format
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC)}, &model.User{BlockFilterEntryRules: "UnknownRuleType=test"}, false},
{&model.Feed{ID: 1, BlockFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, &model.Entry{URL: "https://example.com", Title: "Some Example"}, &model.User{}, true},
// Test cases for merged user and feed BlockFilterEntryRules
@ -97,8 +100,11 @@ func TestAllowEntries(t *testing.T) {
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=between:invalid-date,2024-03-15"}, false}, // invalid date format
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=between:2024-03-15,invalid-date"}, false}, // invalid date format
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=between:2024-03-15"}, false}, // missing second date in range
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=abcd"}, false}, // no colon in rule value
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=unknown:2024-03-15"}, false}, // unknown rule type
// Test max-age filter
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=max-age:30d"}, true}, // Entry from Jan 1, 2024 is definitely older than 30 days
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=max-age:invalid"}, false}, // Invalid duration format
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=abcd"}, false}, // no colon in rule value
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC)}, &model.User{KeepFilterEntryRules: "EntryDate=unknown:2024-03-15"}, false}, // unknown rule type
{&model.Feed{ID: 1, KeepFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, &model.Entry{URL: "https://example.com", Title: "Some Example"}, &model.User{}, true},
// Test cases for merged user and feed KeepFilterEntryRules
{&model.Feed{ID: 1, KeepFilterEntryRules: "EntryURL=(?i)website"}, &model.Entry{URL: "https://example.com", Title: "Some Title"}, &model.User{KeepFilterEntryRules: "EntryTitle=(?i)title"}, true}, // User rule matches
@ -114,3 +120,54 @@ func TestAllowEntries(t *testing.T) {
}
}
}
func TestParseDuration(t *testing.T) {
tests := []struct {
input string
expected time.Duration
err bool
}{
{"30d", 30 * 24 * time.Hour, false},
{"1h", time.Hour, false},
{"2m", 2 * time.Minute, false},
{"invalid", 0, true},
{"5x", 0, true}, // Invalid unit
}
for _, test := range tests {
result, err := parseDuration(test.input)
if (err != nil) != test.err {
t.Errorf("parseDuration(%q) error = %v, expected error: %v", test.input, err, test.err)
continue
}
if result != test.expected {
t.Errorf("parseDuration(%q) = %v, expected %v", test.input, result, test.expected)
}
}
}
func TestMaxAgeFilter(t *testing.T) {
now := time.Now()
oldEntry := &model.Entry{
Title: "Old Entry",
Date: now.Add(-48 * time.Hour), // 48 hours ago
}
newEntry := &model.Entry{
Title: "New Entry",
Date: now.Add(-30 * time.Minute), // 30 minutes ago
}
// Test blocking old entries
feed := &model.Feed{ID: 1}
user := &model.User{BlockFilterEntryRules: "EntryDate=max-age:1d"}
// Old entry should be blocked (48 hours > 1 day is true)
if !IsBlockedEntry(feed, oldEntry, user) {
t.Error("Expected old entry to be blocked with max-age:1d")
}
// New entry should not be blocked
if IsBlockedEntry(feed, newEntry, user) {
t.Error("Expected new entry to not be blocked with max-age:1d")
}
}