Skip to content

Commit

Permalink
Implement MSC3664
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Sep 27, 2022
1 parent 61b0553 commit c4ce54d
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 3 deletions.
61 changes: 61 additions & 0 deletions pushrules/condition.go
Expand Up @@ -7,13 +7,17 @@
package pushrules

import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"unicode"

"github.com/tidwall/gjson"

"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules/glob"
)

Expand All @@ -23,6 +27,12 @@ type Room interface {
GetMemberCount() int
}

// EventfulRoom is an extension of Room to support MSC3664.
type EventfulRoom interface {
Room
GetEvent(id.EventID) *event.Event
}

// PushCondKind is the type of a push condition.
type PushCondKind string

Expand All @@ -31,6 +41,11 @@ const (
KindEventMatch PushCondKind = "event_match"
KindContainsDisplayName PushCondKind = "contains_display_name"
KindRoomMemberCount PushCondKind = "room_member_count"

// MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664

KindRelatedEventMatch PushCondKind = "related_event_match"
KindUnstableRelatedEventMatch PushCondKind = "im.nheko.msc3664.related_event_match"
)

// PushCondition wraps a condition that is required for a specific PushRule to be used.
Expand All @@ -44,6 +59,9 @@ type PushCondition struct {
// 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"`

// The relation type for related_event_match from MSC3664
RelType event.RelationType `json:"rel_type,omitempty"`
}

// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions.
Expand All @@ -54,6 +72,8 @@ func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
switch cond.Kind {
case KindEventMatch:
return cond.matchValue(room, evt)
case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
return cond.matchRelatedEvent(room, evt)
case KindContainsDisplayName:
return cond.matchDisplayName(room, evt)
case KindRoomMemberCount:
Expand Down Expand Up @@ -148,6 +168,47 @@ func (cond *PushCondition) matchValue(room Room, evt *event.Event) bool {
}
}

func (cond *PushCondition) getRelationEventID(relatesTo *event.RelatesTo) id.EventID {
if relatesTo == nil {
return ""
}
switch cond.RelType {
case "":
return relatesTo.EventID
case "m.in_reply_to":
if relatesTo.IsFallingBack || relatesTo.InReplyTo == nil {
return ""
}
return relatesTo.InReplyTo.EventID
default:
if relatesTo.Type != cond.RelType {
return ""
}
return relatesTo.EventID
}
}

func (cond *PushCondition) matchRelatedEvent(room Room, evt *event.Event) bool {
var relatesTo *event.RelatesTo
if relatable, ok := evt.Content.Parsed.(event.Relatable); ok {
relatesTo = relatable.OptionalGetRelatesTo()
} else {
res := gjson.GetBytes(evt.Content.VeryRaw, `m\.relates_to`)
if res.Exists() && res.IsObject() {
_ = json.Unmarshal([]byte(res.Str), &relatesTo)
}
}
if evtID := cond.getRelationEventID(relatesTo); evtID == "" {
return false
} else if eventfulRoom, ok := room.(EventfulRoom); !ok {
return false
} else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil {
return false
} else {
return cond.matchValue(room, evt)
}
}

func (cond *PushCondition) matchDisplayName(room Room, evt *event.Event) bool {
displayname := room.GetOwnDisplayname()
if len(displayname) == 0 {
Expand Down
8 changes: 8 additions & 0 deletions pushrules/condition_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"

"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
)

Expand Down Expand Up @@ -104,12 +105,15 @@ func TestPushCondition_Match_InvalidKind(t *testing.T) {
type FakeRoom struct {
members map[string]*event.MemberEventContent
owner string

events map[id.EventID]*event.Event
}

func newFakeRoom(memberCount int) *FakeRoom {
room := &FakeRoom{
owner: "@tulir:maunium.net",
members: make(map[string]*event.MemberEventContent),
events: make(map[id.EventID]*event.Event),
}

if memberCount >= 1 {
Expand Down Expand Up @@ -141,3 +145,7 @@ func (fr *FakeRoom) GetOwnDisplayname() string {
}
return ""
}

func (fr *FakeRoom) GetEvent(evtID id.EventID) *event.Event {
return fr.events[evtID]
}
34 changes: 31 additions & 3 deletions pushrules/rule_test.go
Expand Up @@ -64,7 +64,7 @@ func TestPushRule_Match_Conditions_NestedKey_Boolean(t *testing.T) {
Conditions: []*pushrules.PushCondition{cond1},
}

evt := newFakeEvent(event.EventMessage, &event.MemberEventContent{
evt := newFakeEvent(event.StateMember, &event.MemberEventContent{
Membership: "invite",
})
assert.False(t, rule.Match(blankTestRoom, evt))
Expand All @@ -86,7 +86,7 @@ func TestPushRule_Match_Conditions_EscapedKey(t *testing.T) {
Conditions: []*pushrules.PushCondition{cond1},
}

evt := newFakeEvent(event.EventMessage, &event.MemberEventContent{
evt := newFakeEvent(event.StateMember, &event.MemberEventContent{
Membership: "invite",
})
assert.False(t, rule.Match(blankTestRoom, evt))
Expand All @@ -102,7 +102,7 @@ func TestPushRule_Match_Conditions_EscapedKey_NoNesting(t *testing.T) {
Conditions: []*pushrules.PushCondition{cond1},
}

evt := newFakeEvent(event.EventMessage, &event.MemberEventContent{
evt := newFakeEvent(event.StateMember, &event.MemberEventContent{
Membership: "invite",
})
assert.False(t, rule.Match(blankTestRoom, evt))
Expand All @@ -112,6 +112,34 @@ func TestPushRule_Match_Conditions_EscapedKey_NoNesting(t *testing.T) {
assert.False(t, rule.Match(blankTestRoom, evt))
}

func TestPushRule_Match_Conditions_RelatedEvent(t *testing.T) {
cond1 := &pushrules.PushCondition{
Kind: pushrules.KindRelatedEventMatch,
Key: "sender",
Pattern: "@tulir:maunium.net",
}
rule := &pushrules.PushRule{
Type: pushrules.OverrideRule,
Enabled: true,
Conditions: []*pushrules.PushCondition{cond1},
}

evt := newFakeEvent(event.EventReaction, &event.ReactionEventContent{
RelatesTo: event.RelatesTo{
Type: event.RelAnnotation,
EventID: "$meow",
Key: "🐈️",
},
})
roomWithEvent := newFakeRoom(1)
assert.False(t, rule.Match(roomWithEvent, evt))
roomWithEvent.events["$meow"] = newFakeEvent(event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgEmote,
Body: "is testing pushrules",
})
assert.True(t, rule.Match(roomWithEvent, evt))
}

func TestPushRule_Match_Conditions_Disabled(t *testing.T) {
cond1 := newMatchPushCondition("content.msgtype", "m.emote")
cond2 := newMatchPushCondition("content.body", "*pushrules")
Expand Down

0 comments on commit c4ce54d

Please sign in to comment.