feat: implement rule engine, worker pool, event bus, and readability scraping with enhanced logging and content sanitization.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
|
||||
)
|
||||
|
||||
type Evaluator struct{}
|
||||
|
||||
func NewEvaluator() *Evaluator {
|
||||
return &Evaluator{}
|
||||
}
|
||||
|
||||
// Evaluate checks whether a FeedEntry satisfies a Rule's conditions.
|
||||
func (e *Evaluator) Evaluate(rule models.Rule, entry models.FeedEntry) bool {
|
||||
if len(rule.Conditions) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
matchMode := strings.ToLower(rule.MatchMode)
|
||||
if matchMode == "" || matchMode == "all" {
|
||||
for _, cond := range rule.Conditions {
|
||||
if !e.evaluateCondition(cond, entry) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchMode == "any"
|
||||
for _, cond := range rule.Conditions {
|
||||
if e.evaluateCondition(cond, entry) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *Evaluator) evaluateCondition(cond models.RuleCondition, entry models.FeedEntry) bool {
|
||||
actualValue := e.resolveField(cond.Field, entry)
|
||||
return e.compare(actualValue, cond.Operator, cond.Value)
|
||||
}
|
||||
|
||||
func (e *Evaluator) resolveField(field string, entry models.FeedEntry) interface{} {
|
||||
switch strings.ToLower(field) {
|
||||
case "feed_id":
|
||||
return entry.FeedID
|
||||
case "feed_title":
|
||||
return entry.FeedTitle
|
||||
case "title":
|
||||
return entry.Title
|
||||
case "author":
|
||||
return entry.Author
|
||||
case "url":
|
||||
return entry.URL
|
||||
case "content", "rss_content":
|
||||
return entry.Content
|
||||
case "is_read":
|
||||
return entry.IsRead
|
||||
case "is_starred":
|
||||
return entry.IsStarred
|
||||
}
|
||||
|
||||
// Variant field resolution: "variants.keywords", "variants.readability_full_text", "variants.tags"
|
||||
if strings.HasPrefix(field, "variants.") {
|
||||
variantKey := strings.TrimPrefix(field, "variants.")
|
||||
if entry.Variants != nil {
|
||||
if v, ok := entry.Variants[variantKey]; ok {
|
||||
return v.TextValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Evaluator) compare(actual interface{}, op string, expected interface{}) bool {
|
||||
if actual == nil {
|
||||
return op == "neq"
|
||||
}
|
||||
|
||||
actualStr := fmt.Sprintf("%v", actual)
|
||||
expectedStr := fmt.Sprintf("%v", expected)
|
||||
|
||||
switch strings.ToLower(op) {
|
||||
case "eq", "==":
|
||||
return strings.EqualFold(actualStr, expectedStr)
|
||||
case "neq", "!=":
|
||||
return !strings.EqualFold(actualStr, expectedStr)
|
||||
case "contains":
|
||||
return strings.Contains(strings.ToLower(actualStr), strings.ToLower(expectedStr))
|
||||
case "not_contains":
|
||||
return !strings.Contains(strings.ToLower(actualStr), strings.ToLower(expectedStr))
|
||||
case "in":
|
||||
// expected can be []interface{}, []string, or comma-separated string
|
||||
switch v := expected.(type) {
|
||||
case []interface{}:
|
||||
for _, item := range v {
|
||||
if strings.EqualFold(actualStr, fmt.Sprintf("%v", item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case []string:
|
||||
for _, item := range v {
|
||||
if strings.EqualFold(actualStr, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case string:
|
||||
items := strings.Split(v, ",")
|
||||
for _, item := range items {
|
||||
if strings.EqualFold(strings.TrimSpace(actualStr), strings.TrimSpace(item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
case "matches", "regex":
|
||||
re, err := regexp.Compile(expectedStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return re.MatchString(actualStr)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user