feat: evolve rate-filter to a more generic filter
This commit is contained in:
133
internal/automation/filter.go
Normal file
133
internal/automation/filter.go
Normal file
@ -0,0 +1,133 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// applyFilters implements the "filters" ssm feature: for each rule, it
|
||||
// scopes down to the proxies matched by rule.Match, decides which of those
|
||||
// are unwanted, and strips them out of every proxy-group's `proxies` list
|
||||
// in finalConfig — both groups merged in from subscriptions and ones built
|
||||
// by a proxy-groups rule. A scoped proxy is unwanted if rule.Compares is
|
||||
// empty (Match alone acts as a denylist), or if it satisfies every entry in
|
||||
// Compares (AND'd) when Compares is non-empty; a proxy whose name doesn't
|
||||
// match a Compares entry's Pattern at all can't be evaluated and is left
|
||||
// alone. Returns one error per rule with an invalid pattern/operator/match;
|
||||
// other rules still run.
|
||||
func applyFilters(finalConfig map[string]interface{}, rules []FilterRule, proxies []ProxyRef) []error {
|
||||
var errs []error
|
||||
|
||||
for _, rule := range rules {
|
||||
candidates, err := matchProxies(proxies, rule.Match)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
compares, err := compileCompares(rule.Compares)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
unwanted := make(map[string]bool, len(candidates))
|
||||
for _, p := range candidates {
|
||||
if len(compares) == 0 || satisfiesCompares(compares, p.OriginalName) {
|
||||
unwanted[p.DisplayName] = true
|
||||
}
|
||||
}
|
||||
if len(unwanted) == 0 {
|
||||
fmt.Printf("ℹ️ ssm: filter '%s' matched 0 proxies to remove\n", rule.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
removeFromProxyGroups(finalConfig, unwanted)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// removeFromProxyGroups strips every proxy in unwanted out of each proxy
|
||||
// group's `proxies` list, leaving group references, DIRECT/REJECT, etc.
|
||||
// untouched.
|
||||
func removeFromProxyGroups(finalConfig map[string]interface{}, unwanted map[string]bool) {
|
||||
groups, _ := finalConfig["proxy-groups"].([]interface{})
|
||||
for _, g := range groups {
|
||||
group, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
list, ok := group["proxies"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kept := make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
if name, ok := item.(string); ok && unwanted[name] {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, item)
|
||||
}
|
||||
group["proxies"] = kept
|
||||
}
|
||||
}
|
||||
|
||||
type compiledCompare struct {
|
||||
re *regexp.Regexp
|
||||
cmp func(a, b float64) bool
|
||||
value float64
|
||||
}
|
||||
|
||||
func compileCompares(rules []CompareRule) ([]compiledCompare, error) {
|
||||
compiled := make([]compiledCompare, 0, len(rules))
|
||||
for _, c := range rules {
|
||||
re, err := regexp.Compile(c.Pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid pattern %q: %w", c.Pattern, err)
|
||||
}
|
||||
cmp, err := comparator(c.Operator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiled = append(compiled, compiledCompare{re: re, cmp: cmp, value: c.Value})
|
||||
}
|
||||
return compiled, nil
|
||||
}
|
||||
|
||||
func satisfiesCompares(compares []compiledCompare, name string) bool {
|
||||
for _, c := range compares {
|
||||
m := c.re.FindStringSubmatch(name)
|
||||
if len(m) < 2 {
|
||||
return false
|
||||
}
|
||||
extracted, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !c.cmp(extracted, c.value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func comparator(op string) (func(a, b float64) bool, error) {
|
||||
switch op {
|
||||
case "<":
|
||||
return func(a, b float64) bool { return a < b }, nil
|
||||
case "<=":
|
||||
return func(a, b float64) bool { return a <= b }, nil
|
||||
case ">":
|
||||
return func(a, b float64) bool { return a > b }, nil
|
||||
case ">=":
|
||||
return func(a, b float64) bool { return a >= b }, nil
|
||||
case "==", "=":
|
||||
return func(a, b float64) bool { return a == b }, nil
|
||||
case "!=":
|
||||
return func(a, b float64) bool { return a != b }, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operator %q (expected <, <=, >, >=, ==, !=)", op)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user