Implement new push rule condition kinds (#120)

pull/130/head
Tulir Asokan 2023-05-24 15:09:43 +03:00 committed by GitHub
parent 1a6af5d3ee
commit 49a6de3c27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 24 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Tulir Asokan
// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -38,9 +38,11 @@ type PushCondKind string
// The allowed push condition kinds as specified in https://spec.matrix.org/v1.2/client-server-api/#conditions-1
const (
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"
KindEventPropertyIs PushCondKind = "event_property_is"
KindEventPropertyContains PushCondKind = "event_property_contains"
// MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664
@ -56,6 +58,8 @@ type PushCondition struct {
Key string `json:"key,omitempty"`
// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
Pattern string `json:"pattern,omitempty"`
// The exact value to match the field against. Only applicable if kind is EventPropertyIs or EventPropertyContains.
Value any `json:"value,omitempty"`
// The condition that needs to be fulfilled for RoomMemberCount-type conditions.
// A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found.
MemberCountCondition string `json:"is,omitempty"`
@ -70,8 +74,8 @@ var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
// Match checks if this condition is fulfilled for the given event in the given room.
func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
switch cond.Kind {
case KindEventMatch:
return cond.matchValue(room, evt)
case KindEventMatch, KindEventPropertyIs, KindEventPropertyContains:
return cond.matchValue(evt)
case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
return cond.matchRelatedEvent(room, evt)
case KindContainsDisplayName:
@ -101,13 +105,13 @@ func splitWithEscaping(s string, separator, escape byte) []string {
return tokens
}
func hackyNestedGet(data map[string]interface{}, path []string) (interface{}, bool) {
func hackyNestedGet(data map[string]any, path []string) (any, bool) {
val, ok := data[path[0]]
if len(path) == 1 {
// We don't have any more path parts, return the value regardless of whether it exists or not.
return val, ok
} else if ok {
if mapVal, ok := val.(map[string]interface{}); ok {
if mapVal, ok := val.(map[string]any); ok {
val, ok = hackyNestedGet(mapVal, path[1:])
if ok {
return val, true
@ -138,37 +142,103 @@ func stringifyForPushCondition(val interface{}) string {
}
}
func (cond *PushCondition) matchValue(room Room, evt *event.Event) bool {
func (cond *PushCondition) getValue(evt *event.Event) (any, bool) {
key, subkey, _ := strings.Cut(cond.Key, ".")
pattern, err := glob.Compile(cond.Pattern)
if err != nil {
return false
}
switch key {
case "type":
return pattern.MatchString(evt.Type.String())
return evt.Type.Type, true
case "sender":
return pattern.MatchString(string(evt.Sender))
return evt.Sender.String(), true
case "room_id":
return pattern.MatchString(string(evt.RoomID))
return evt.RoomID.String(), true
case "state_key":
if evt.StateKey == nil {
return false
return nil, false
}
return pattern.MatchString(*evt.StateKey)
return *evt.StateKey, true
case "content":
// Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873
splitKey := splitWithEscaping(subkey, '.', '\\')
// Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873
val, ok := hackyNestedGet(evt.Content.Raw, splitKey)
if !ok {
return cond.Pattern == ""
return hackyNestedGet(evt.Content.Raw, splitKey)
default:
return nil, false
}
}
func numberToInt64(a any) int64 {
switch typed := a.(type) {
case float64:
return int64(typed)
case float32:
return int64(typed)
case int:
return int64(typed)
case int8:
return int64(typed)
case int16:
return int64(typed)
case int32:
return int64(typed)
case int64:
return typed
case uint:
return int64(typed)
case uint8:
return int64(typed)
case uint16:
return int64(typed)
case uint32:
return int64(typed)
case uint64:
return int64(typed)
default:
return 0
}
}
func valueEquals(a, b any) bool {
// Convert floats to ints when comparing numbers (the JSON parser generates floats, but Matrix only allows integers)
// Also allow other numeric types in case something generates events manually without json
switch a.(type) {
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
switch b.(type) {
case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return numberToInt64(a) == numberToInt64(b)
}
}
return a == b
}
func (cond *PushCondition) matchValue(evt *event.Event) bool {
val, ok := cond.getValue(evt)
if !ok {
return false
}
switch cond.Kind {
case KindEventMatch, KindRelatedEventMatch, KindUnstableRelatedEventMatch:
pattern, err := glob.Compile(cond.Pattern)
if err != nil {
return false
}
return pattern.MatchString(stringifyForPushCondition(val))
default:
case KindEventPropertyIs:
return valueEquals(val, cond.Value)
case KindEventPropertyContains:
valArr, ok := val.([]any)
if !ok {
return false
}
for _, item := range valArr {
if valueEquals(item, cond.Value) {
return true
}
}
return false
default:
panic(fmt.Errorf("matchValue called for unknown condition kind %s", cond.Kind))
}
}
@ -209,7 +279,7 @@ func (cond *PushCondition) matchRelatedEvent(room Room, evt *event.Event) bool {
} else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil {
return false
} else {
return cond.matchValue(room, evt)
return cond.matchValue(evt)
}
}

View File

@ -0,0 +1,91 @@
// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package pushrules_test
import (
"maunium.net/go/mautrix/event"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPushCondition_Match_KindEventPropertyIs_MsgType(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.msgtype", "m.emote")
evt := newFakeEvent(event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgEmote,
Body: "tests gomuks pushconditions",
})
assert.True(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_MsgType_Fail(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.msgtype", "m.emote")
evt := newFakeEvent(event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgText,
Body: "I'm testing gomuks pushconditions",
})
assert.False(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Integer(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", 5)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": 5})
assert.True(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Integer_NoMatch(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", 0)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": "NaN"})
assert.False(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_String(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", "foo")
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": "foo"})
assert.True(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_String_NoMatch(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", "foo")
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": "foo!"})
assert.False(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Null(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", nil)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": nil})
assert.True(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Null_NoMatch(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", nil)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": "a"})
assert.False(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Bool(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", false)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": false})
assert.True(t, condition.Match(blankTestRoom, evt))
condition = newEventPropertyIsPushCondition("content.meow", true)
evt = newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": true})
assert.True(t, condition.Match(blankTestRoom, evt))
}
func TestPushCondition_Match_KindEventPropertyIs_Bool_NoMatch(t *testing.T) {
condition := newEventPropertyIsPushCondition("content.meow", false)
evt := newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": true})
assert.False(t, condition.Match(blankTestRoom, evt))
condition = newEventPropertyIsPushCondition("content.meow", true)
evt = newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": false})
assert.False(t, condition.Match(blankTestRoom, evt))
condition = newEventPropertyIsPushCondition("content.meow", false)
evt = newFakeEvent(event.NewEventType("m.room.foo"), map[string]any{"meow": ""})
assert.False(t, condition.Match(blankTestRoom, evt))
}

View File

@ -94,6 +94,22 @@ func newMatchPushCondition(key, pattern string) *pushrules.PushCondition {
}
}
func newEventPropertyIsPushCondition(key string, value any) *pushrules.PushCondition {
return &pushrules.PushCondition{
Kind: pushrules.KindEventPropertyIs,
Key: key,
Value: value,
}
}
func newEventPropertyContainsPushCondition(key string, value any) *pushrules.PushCondition {
return &pushrules.PushCondition{
Kind: pushrules.KindEventPropertyContains,
Key: key,
Value: value,
}
}
func TestPushCondition_Match_InvalidKind(t *testing.T) {
condition := &pushrules.PushCondition{
Kind: pushrules.PushCondKind("invalid"),

View File

@ -134,6 +134,12 @@ func (rule *PushRule) Match(room Room, evt *event.Event) bool {
if rule == nil || !rule.Enabled {
return false
}
if rule.RuleID == ".m.rule.contains_display_name" || rule.RuleID == ".m.rule.contains_user_name" || rule.RuleID == ".m.rule.roomnotif" {
if _, containsMentions := evt.Content.Raw["m.mentions"]; containsMentions {
// Disable legacy mention push rules when the event contains the new mentions key
return false
}
}
switch rule.Type {
case OverrideRule, UnderrideRule:
return rule.matchConditions(room, evt)