dendrite/internal/pushrules/evaluate.go
Neil Alexander f009e54181
Push rule evaluation tweaks (#2897)
This tweaks push rule evaluation:

1. to be more strict around pattern matching and to not match empty
patterns
3. to bail if we come across a `dont_notify`, since cycles after that
are wasted
4. refactors `ActionsToTweaks` to make a bit more sense
2022-11-30 12:54:37 +00:00

178 lines
4.7 KiB
Go

package pushrules
import (
"encoding/json"
"fmt"
"strings"
"github.com/matrix-org/gomatrixserverlib"
)
// A RuleSetEvaluator encapsulates context to evaluate an event
// against a rule set.
type RuleSetEvaluator struct {
ec EvaluationContext
ruleSet []kindAndRules
}
// An EvaluationContext gives a RuleSetEvaluator access to the
// environment, for rules that require that.
type EvaluationContext interface {
// UserDisplayName returns the current user's display name.
UserDisplayName() string
// RoomMemberCount returns the number of members in the room of
// the current event.
RoomMemberCount() (int, error)
// HasPowerLevel returns whether the user has at least the given
// power in the room of the current event.
HasPowerLevel(userID, levelKey string) (bool, error)
}
// A kindAndRules is just here to simplify iteration of the (ordered)
// kinds of rules.
type kindAndRules struct {
Kind Kind
Rules []*Rule
}
// NewRuleSetEvaluator creates a new evaluator for the given rule set.
func NewRuleSetEvaluator(ec EvaluationContext, ruleSet *RuleSet) *RuleSetEvaluator {
return &RuleSetEvaluator{
ec: ec,
ruleSet: []kindAndRules{
{OverrideKind, ruleSet.Override},
{ContentKind, ruleSet.Content},
{RoomKind, ruleSet.Room},
{SenderKind, ruleSet.Sender},
{UnderrideKind, ruleSet.Underride},
},
}
}
// MatchEvent returns the first matching rule. Returns nil if there
// was no match rule.
func (rse *RuleSetEvaluator) MatchEvent(event *gomatrixserverlib.Event) (*Rule, error) {
// TODO: server-default rules have lower priority than user rules,
// but they are stored together with the user rules. It's a bit
// unclear what the specification (11.14.1.4 Predefined rules)
// means the ordering should be.
//
// The most reasonable interpretation is that default overrides
// still have lower priority than user content rules, so we
// iterate twice.
for _, rsat := range rse.ruleSet {
for _, defRules := range []bool{false, true} {
for _, rule := range rsat.Rules {
if rule.Default != defRules {
continue
}
ok, err := ruleMatches(rule, rsat.Kind, event, rse.ec)
if err != nil {
return nil, err
}
if ok {
return rule, nil
}
}
}
}
// No matching rule.
return nil, nil
}
func ruleMatches(rule *Rule, kind Kind, event *gomatrixserverlib.Event, ec EvaluationContext) (bool, error) {
if !rule.Enabled {
return false, nil
}
switch kind {
case OverrideKind, UnderrideKind:
for _, cond := range rule.Conditions {
ok, err := conditionMatches(cond, event, ec)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
}
return true, nil
case ContentKind:
// TODO: "These configure behaviour for (unencrypted) messages
// that match certain patterns." - Does that mean "content.body"?
return patternMatches("content.body", rule.Pattern, event)
case RoomKind:
return rule.RuleID == event.RoomID(), nil
case SenderKind:
return rule.RuleID == event.Sender(), nil
default:
return false, nil
}
}
func conditionMatches(cond *Condition, event *gomatrixserverlib.Event, ec EvaluationContext) (bool, error) {
switch cond.Kind {
case EventMatchCondition:
return patternMatches(cond.Key, cond.Pattern, event)
case ContainsDisplayNameCondition:
return patternMatches("content.body", ec.UserDisplayName(), event)
case RoomMemberCountCondition:
cmp, err := parseRoomMemberCountCondition(cond.Is)
if err != nil {
return false, fmt.Errorf("parsing room_member_count condition: %w", err)
}
n, err := ec.RoomMemberCount()
if err != nil {
return false, fmt.Errorf("RoomMemberCount failed: %w", err)
}
return cmp(n), nil
case SenderNotificationPermissionCondition:
return ec.HasPowerLevel(event.Sender(), cond.Key)
default:
return false, nil
}
}
func patternMatches(key, pattern string, event *gomatrixserverlib.Event) (bool, error) {
// It doesn't make sense for an empty pattern to match anything.
if pattern == "" {
return false, nil
}
re, err := globToRegexp(pattern)
if err != nil {
return false, err
}
var eventMap map[string]interface{}
if err = json.Unmarshal(event.JSON(), &eventMap); err != nil {
return false, fmt.Errorf("parsing event: %w", err)
}
// From the spec:
// "If the property specified by key is completely absent from
// the event, or does not have a string value, then the condition
// will not match, even if pattern is *."
v, err := lookupMapPath(strings.Split(key, "."), eventMap)
if err != nil {
// An unknown path is a benign error that shouldn't stop rule
// processing. It's just a non-match.
return false, nil
}
if _, ok := v.(string); !ok {
// A non-string never matches.
return false, nil
}
return re.MatchString(fmt.Sprint(v)), nil
}