mirror of https://github.com/mautrix/go.git
Implement new push rule condition kinds (#120)
parent
1a6af5d3ee
commit
49a6de3c27
|
@ -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
|
// 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
|
// 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
|
// The allowed push condition kinds as specified in https://spec.matrix.org/v1.2/client-server-api/#conditions-1
|
||||||
const (
|
const (
|
||||||
KindEventMatch PushCondKind = "event_match"
|
KindEventMatch PushCondKind = "event_match"
|
||||||
KindContainsDisplayName PushCondKind = "contains_display_name"
|
KindContainsDisplayName PushCondKind = "contains_display_name"
|
||||||
KindRoomMemberCount PushCondKind = "room_member_count"
|
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
|
// MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664
|
||||||
|
|
||||||
|
@ -56,6 +58,8 @@ type PushCondition struct {
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
|
// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
|
||||||
Pattern string `json:"pattern,omitempty"`
|
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.
|
// 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.
|
// A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found.
|
||||||
MemberCountCondition string `json:"is,omitempty"`
|
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.
|
// 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 {
|
func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
|
||||||
switch cond.Kind {
|
switch cond.Kind {
|
||||||
case KindEventMatch:
|
case KindEventMatch, KindEventPropertyIs, KindEventPropertyContains:
|
||||||
return cond.matchValue(room, evt)
|
return cond.matchValue(evt)
|
||||||
case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
|
case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
|
||||||
return cond.matchRelatedEvent(room, evt)
|
return cond.matchRelatedEvent(room, evt)
|
||||||
case KindContainsDisplayName:
|
case KindContainsDisplayName:
|
||||||
|
@ -101,13 +105,13 @@ func splitWithEscaping(s string, separator, escape byte) []string {
|
||||||
return tokens
|
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]]
|
val, ok := data[path[0]]
|
||||||
if len(path) == 1 {
|
if len(path) == 1 {
|
||||||
// We don't have any more path parts, return the value regardless of whether it exists or not.
|
// We don't have any more path parts, return the value regardless of whether it exists or not.
|
||||||
return val, ok
|
return val, ok
|
||||||
} else if 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:])
|
val, ok = hackyNestedGet(mapVal, path[1:])
|
||||||
if ok {
|
if ok {
|
||||||
return val, true
|
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, ".")
|
key, subkey, _ := strings.Cut(cond.Key, ".")
|
||||||
|
|
||||||
pattern, err := glob.Compile(cond.Pattern)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case "type":
|
case "type":
|
||||||
return pattern.MatchString(evt.Type.String())
|
return evt.Type.Type, true
|
||||||
case "sender":
|
case "sender":
|
||||||
return pattern.MatchString(string(evt.Sender))
|
return evt.Sender.String(), true
|
||||||
case "room_id":
|
case "room_id":
|
||||||
return pattern.MatchString(string(evt.RoomID))
|
return evt.RoomID.String(), true
|
||||||
case "state_key":
|
case "state_key":
|
||||||
if evt.StateKey == nil {
|
if evt.StateKey == nil {
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
return pattern.MatchString(*evt.StateKey)
|
return *evt.StateKey, true
|
||||||
case "content":
|
case "content":
|
||||||
// Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873
|
// Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873
|
||||||
splitKey := splitWithEscaping(subkey, '.', '\\')
|
splitKey := splitWithEscaping(subkey, '.', '\\')
|
||||||
// Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873
|
// Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873
|
||||||
val, ok := hackyNestedGet(evt.Content.Raw, splitKey)
|
return hackyNestedGet(evt.Content.Raw, splitKey)
|
||||||
if !ok {
|
default:
|
||||||
return cond.Pattern == ""
|
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))
|
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
|
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 {
|
} else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
return cond.matchValue(room, evt)
|
return cond.matchValue(evt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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) {
|
func TestPushCondition_Match_InvalidKind(t *testing.T) {
|
||||||
condition := &pushrules.PushCondition{
|
condition := &pushrules.PushCondition{
|
||||||
Kind: pushrules.PushCondKind("invalid"),
|
Kind: pushrules.PushCondKind("invalid"),
|
||||||
|
|
|
@ -134,6 +134,12 @@ func (rule *PushRule) Match(room Room, evt *event.Event) bool {
|
||||||
if rule == nil || !rule.Enabled {
|
if rule == nil || !rule.Enabled {
|
||||||
return false
|
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 {
|
switch rule.Type {
|
||||||
case OverrideRule, UnderrideRule:
|
case OverrideRule, UnderrideRule:
|
||||||
return rule.matchConditions(room, evt)
|
return rule.matchConditions(room, evt)
|
||||||
|
|
Loading…
Reference in New Issue