2022-03-03 11:40:53 +00:00
|
|
|
package pushrules
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/matrix-org/gomatrixserverlib"
|
2023-06-06 20:55:18 +00:00
|
|
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
2022-03-03 11:40:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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.
|
2023-06-06 20:55:18 +00:00
|
|
|
func (rse *RuleSetEvaluator) MatchEvent(event gomatrixserverlib.PDU, userIDForSender spec.UserIDForSender) (*Rule, error) {
|
2022-03-03 11:40:53 +00:00
|
|
|
// 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
|
|
|
|
}
|
2023-06-06 20:55:18 +00:00
|
|
|
ok, err := ruleMatches(rule, rsat.Kind, event, rse.ec, userIDForSender)
|
2022-03-03 11:40:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if ok {
|
|
|
|
return rule, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No matching rule.
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2023-06-06 20:55:18 +00:00
|
|
|
func ruleMatches(rule *Rule, kind Kind, event gomatrixserverlib.PDU, ec EvaluationContext, userIDForSender spec.UserIDForSender) (bool, error) {
|
2022-03-03 11:40:53 +00:00
|
|
|
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"?
|
2022-12-23 11:52:47 +00:00
|
|
|
if rule.Pattern == nil {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return patternMatches("content.body", *rule.Pattern, event)
|
2022-03-03 11:40:53 +00:00
|
|
|
|
|
|
|
case RoomKind:
|
|
|
|
return rule.RuleID == event.RoomID(), nil
|
|
|
|
|
|
|
|
case SenderKind:
|
2023-06-06 20:55:18 +00:00
|
|
|
userID := ""
|
|
|
|
sender, err := userIDForSender(event.RoomID(), event.SenderID())
|
|
|
|
if err == nil {
|
|
|
|
userID = sender.String()
|
|
|
|
}
|
|
|
|
return rule.RuleID == userID, nil
|
2022-03-03 11:40:53 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 15:00:22 +00:00
|
|
|
func conditionMatches(cond *Condition, event gomatrixserverlib.PDU, ec EvaluationContext) (bool, error) {
|
2022-03-03 11:40:53 +00:00
|
|
|
switch cond.Kind {
|
|
|
|
case EventMatchCondition:
|
2022-12-23 11:52:47 +00:00
|
|
|
if cond.Pattern == nil {
|
|
|
|
return false, fmt.Errorf("missing condition pattern")
|
|
|
|
}
|
|
|
|
return patternMatches(cond.Key, *cond.Pattern, event)
|
2022-03-03 11:40:53 +00:00
|
|
|
|
|
|
|
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:
|
2023-06-06 20:55:18 +00:00
|
|
|
return ec.HasPowerLevel(event.SenderID(), cond.Key)
|
2022-03-03 11:40:53 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 15:00:22 +00:00
|
|
|
func patternMatches(key, pattern string, event gomatrixserverlib.PDU) (bool, error) {
|
2022-11-30 12:54:37 +00:00
|
|
|
// It doesn't make sense for an empty pattern to match anything.
|
|
|
|
if pattern == "" {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2022-03-03 11:40:53 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-11-30 12:54:37 +00:00
|
|
|
// 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 *."
|
2022-03-03 11:40:53 +00:00
|
|
|
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
|
|
|
|
}
|
2022-11-30 12:54:37 +00:00
|
|
|
if _, ok := v.(string); !ok {
|
|
|
|
// A non-string never matches.
|
|
|
|
return false, nil
|
|
|
|
}
|
2022-03-03 11:40:53 +00:00
|
|
|
|
|
|
|
return re.MatchString(fmt.Sprint(v)), nil
|
|
|
|
}
|