mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
refactor(filter): avoid code duplication between IsBlockedEntry and IsAllowedEntry functions
This commit is contained in:
parent
bc6ab44ff2
commit
c12476c1a9
2 changed files with 83 additions and 110 deletions
|
@ -13,149 +13,121 @@ import (
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO factorize isBlockedEntry and isAllowedEntry
|
type filterActionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
filterActionBlock filterActionType = "block"
|
||||||
|
filterActionAllow filterActionType = "allow"
|
||||||
|
)
|
||||||
|
|
||||||
func IsBlockedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
|
func IsBlockedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
|
||||||
|
// Check user-defined block rules first
|
||||||
if user.BlockFilterEntryRules != "" {
|
if user.BlockFilterEntryRules != "" {
|
||||||
for rule := range strings.SplitSeq(user.BlockFilterEntryRules, "\n") {
|
if matchesUserRules(user.BlockFilterEntryRules, entry, feed, filterActionBlock) {
|
||||||
match := false
|
|
||||||
|
|
||||||
parts := strings.SplitN(rule, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey, ruleValue := parts[0], parts[1]
|
|
||||||
|
|
||||||
switch ruleKey {
|
|
||||||
case "EntryDate":
|
|
||||||
match = isDateMatchingPattern(ruleValue, entry.Date)
|
|
||||||
case "EntryTitle":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Title)
|
|
||||||
case "EntryURL":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.URL)
|
|
||||||
case "EntryCommentsURL":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.CommentsURL)
|
|
||||||
case "EntryContent":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Content)
|
|
||||||
case "EntryAuthor":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Author)
|
|
||||||
case "EntryTag":
|
|
||||||
match = containsRegexPattern(ruleValue, entry.Tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
if match {
|
|
||||||
slog.Debug("Blocking entry based on rule",
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Int64("feed_id", feed.ID),
|
|
||||||
slog.String("feed_url", feed.FeedURL),
|
|
||||||
slog.String("rule", rule),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check feed-level blocklist rules
|
||||||
if feed.BlocklistRules == "" {
|
if feed.BlocklistRules == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
compiledBlocklist, err := regexp.Compile(feed.BlocklistRules)
|
return matchesFeedRules(feed.BlocklistRules, entry, feed, filterActionBlock)
|
||||||
if err != nil {
|
|
||||||
slog.Debug("Failed on regexp compilation",
|
|
||||||
slog.String("pattern", feed.BlocklistRules),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
containsBlockedTag := slices.ContainsFunc(entry.Tags, func(tag string) bool {
|
|
||||||
return compiledBlocklist.MatchString(tag)
|
|
||||||
})
|
|
||||||
|
|
||||||
if compiledBlocklist.MatchString(entry.URL) || compiledBlocklist.MatchString(entry.Title) || compiledBlocklist.MatchString(entry.Author) || containsBlockedTag {
|
|
||||||
slog.Debug("Blocking entry based on rule",
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Int64("feed_id", feed.ID),
|
|
||||||
slog.String("feed_url", feed.FeedURL),
|
|
||||||
slog.String("rule", feed.BlocklistRules),
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsAllowedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
|
func IsAllowedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
|
||||||
|
// Check user-defined keep rules first
|
||||||
if user.KeepFilterEntryRules != "" {
|
if user.KeepFilterEntryRules != "" {
|
||||||
for rule := range strings.SplitSeq(user.KeepFilterEntryRules, "\n") {
|
return matchesUserRules(user.KeepFilterEntryRules, entry, feed, filterActionAllow)
|
||||||
match := false
|
|
||||||
|
|
||||||
parts := strings.SplitN(rule, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleKey, ruleValue := parts[0], parts[1]
|
|
||||||
|
|
||||||
switch ruleKey {
|
|
||||||
case "EntryDate":
|
|
||||||
match = isDateMatchingPattern(ruleValue, entry.Date)
|
|
||||||
case "EntryTitle":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Title)
|
|
||||||
case "EntryURL":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.URL)
|
|
||||||
case "EntryCommentsURL":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.CommentsURL)
|
|
||||||
case "EntryContent":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Content)
|
|
||||||
case "EntryAuthor":
|
|
||||||
match, _ = regexp.MatchString(ruleValue, entry.Author)
|
|
||||||
case "EntryTag":
|
|
||||||
match = containsRegexPattern(ruleValue, entry.Tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
if match {
|
|
||||||
slog.Debug("Allowing entry based on rule",
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Int64("feed_id", feed.ID),
|
|
||||||
slog.String("feed_url", feed.FeedURL),
|
|
||||||
slog.String("rule", rule),
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check feed-level keeplist rules
|
||||||
if feed.KeeplistRules == "" {
|
if feed.KeeplistRules == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
compiledKeeplist, err := regexp.Compile(feed.KeeplistRules)
|
return matchesFeedRules(feed.KeeplistRules, entry, feed, filterActionAllow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesUserRules(rules string, entry *model.Entry, feed *model.Feed, filterAction filterActionType) bool {
|
||||||
|
for rule := range strings.SplitSeq(rules, "\n") {
|
||||||
|
if matchesRule(rule, entry) {
|
||||||
|
logFilterAction(entry, feed, rule, filterAction)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesFeedRules(rules string, entry *model.Entry, feed *model.Feed, filterAction filterActionType) bool {
|
||||||
|
compiledRegex, err := regexp.Compile(rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("Failed on regexp compilation",
|
slog.Debug("Failed on regexp compilation",
|
||||||
slog.String("pattern", feed.KeeplistRules),
|
slog.String("pattern", rules),
|
||||||
slog.Any("error", err),
|
slog.Any("error", err),
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
containsAllowedTag := slices.ContainsFunc(entry.Tags, func(tag string) bool {
|
|
||||||
return compiledKeeplist.MatchString(tag)
|
containsMatchingTag := slices.ContainsFunc(entry.Tags, func(tag string) bool {
|
||||||
|
return compiledRegex.MatchString(tag)
|
||||||
})
|
})
|
||||||
|
|
||||||
if compiledKeeplist.MatchString(entry.URL) || compiledKeeplist.MatchString(entry.Title) || compiledKeeplist.MatchString(entry.Author) || containsAllowedTag {
|
if compiledRegex.MatchString(entry.URL) ||
|
||||||
slog.Debug("Allow entry based on rule",
|
compiledRegex.MatchString(entry.Title) ||
|
||||||
slog.String("entry_url", entry.URL),
|
compiledRegex.MatchString(entry.Author) ||
|
||||||
slog.Int64("feed_id", feed.ID),
|
containsMatchingTag {
|
||||||
slog.String("feed_url", feed.FeedURL),
|
logFilterAction(entry, feed, rules, filterAction)
|
||||||
slog.String("rule", feed.KeeplistRules),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchesRule(rule string, entry *model.Entry) bool {
|
||||||
|
parts := strings.SplitN(rule, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleType, ruleValue := parts[0], parts[1]
|
||||||
|
|
||||||
|
switch ruleType {
|
||||||
|
case "EntryDate":
|
||||||
|
return isDateMatchingPattern(ruleValue, entry.Date)
|
||||||
|
case "EntryTitle":
|
||||||
|
match, _ := regexp.MatchString(ruleValue, entry.Title)
|
||||||
|
return match
|
||||||
|
case "EntryURL":
|
||||||
|
match, _ := regexp.MatchString(ruleValue, entry.URL)
|
||||||
|
return match
|
||||||
|
case "EntryCommentsURL":
|
||||||
|
match, _ := regexp.MatchString(ruleValue, entry.CommentsURL)
|
||||||
|
return match
|
||||||
|
case "EntryContent":
|
||||||
|
match, _ := regexp.MatchString(ruleValue, entry.Content)
|
||||||
|
return match
|
||||||
|
case "EntryAuthor":
|
||||||
|
match, _ := regexp.MatchString(ruleValue, entry.Author)
|
||||||
|
return match
|
||||||
|
case "EntryTag":
|
||||||
|
return containsRegexPattern(ruleValue, entry.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFilterAction(entry *model.Entry, feed *model.Feed, filterRule string, filterAction filterActionType) {
|
||||||
|
slog.Debug("Filtering entry based on rule",
|
||||||
|
slog.Int64("feed_id", feed.ID),
|
||||||
|
slog.String("feed_url", feed.FeedURL),
|
||||||
|
slog.String("entry_url", entry.URL),
|
||||||
|
slog.String("filter_rule", filterRule),
|
||||||
|
slog.Any("filter_action", filterAction),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func isDateMatchingPattern(pattern string, entryDate time.Time) bool {
|
func isDateMatchingPattern(pattern string, entryDate time.Time) bool {
|
||||||
if pattern == "future" {
|
if pattern == "future" {
|
||||||
return entryDate.After(time.Now())
|
return entryDate.After(time.Now())
|
||||||
|
|
|
@ -40,6 +40,7 @@ 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", "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{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},
|
{&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},
|
||||||
|
{&model.Feed{ID: 1}, &model.Entry{Date: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC)}, &model.User{BlockFilterEntryRules: "UnknownRuleType=test"}, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range scenarios {
|
for _, tc := range scenarios {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue