aboutsummaryrefslogtreecommitdiff
path: root/internal/pushrules/evaluate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/pushrules/evaluate.go')
-rw-r--r--internal/pushrules/evaluate.go165
1 files changed, 165 insertions, 0 deletions
diff --git a/internal/pushrules/evaluate.go b/internal/pushrules/evaluate.go
new file mode 100644
index 00000000..df22cb04
--- /dev/null
+++ b/internal/pushrules/evaluate.go
@@ -0,0 +1,165 @@
+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) {
+ 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)
+ }
+ 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
+ }
+
+ return re.MatchString(fmt.Sprint(v)), nil
+}