diff --git a/pushrules/condition.go b/pushrules/condition.go index 0b8aa22b..00a27f5b 100644 --- a/pushrules/condition.go +++ b/pushrules/condition.go @@ -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" ) @@ -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 @@ -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. @@ -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. @@ -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: @@ -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 { diff --git a/pushrules/condition_test.go b/pushrules/condition_test.go index 7b0b9714..c0925f77 100644 --- a/pushrules/condition_test.go +++ b/pushrules/condition_test.go @@ -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" ) @@ -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 { @@ -141,3 +145,7 @@ func (fr *FakeRoom) GetOwnDisplayname() string { } return "" } + +func (fr *FakeRoom) GetEvent(evtID id.EventID) *event.Event { + return fr.events[evtID] +} diff --git a/pushrules/rule_test.go b/pushrules/rule_test.go index 305f8ad8..803c721e 100644 --- a/pushrules/rule_test.go +++ b/pushrules/rule_test.go @@ -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)) @@ -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)) @@ -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)) @@ -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")