summaryrefslogtreecommitdiffstats
path: root/vendor/maunium.net/go/mautrix/pushrules/condition.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/mautrix/pushrules/condition.go')
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/condition.go266
1 files changed, 266 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/pushrules/condition.go b/vendor/maunium.net/go/mautrix/pushrules/condition.go
new file mode 100644
index 00000000..f809f8e7
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/condition.go
@@ -0,0 +1,266 @@
+// Copyright (c) 2022 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
+
+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"
+)
+
+// Room is an interface with the functions that are needed for processing room-specific push conditions
+type Room interface {
+ GetOwnDisplayname() string
+ 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
+
+// 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"
+
+ // 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.
+type PushCondition struct {
+ // The type of the condition.
+ Kind PushCondKind `json:"kind"`
+ // The dot-separated field of the event to match. Only applicable if kind is EventMatch.
+ 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 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.
+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 KindRelatedEventMatch, KindUnstableRelatedEventMatch:
+ return cond.matchRelatedEvent(room, evt)
+ case KindContainsDisplayName:
+ return cond.matchDisplayName(room, evt)
+ case KindRoomMemberCount:
+ return cond.matchMemberCount(room)
+ default:
+ return false
+ }
+}
+
+func splitWithEscaping(s string, separator, escape byte) []string {
+ var token []byte
+ var tokens []string
+ for i := 0; i < len(s); i++ {
+ if s[i] == separator {
+ tokens = append(tokens, string(token))
+ token = token[:0]
+ } else if s[i] == escape && i+1 < len(s) {
+ i++
+ token = append(token, s[i])
+ } else {
+ token = append(token, s[i])
+ }
+ }
+ tokens = append(tokens, string(token))
+ return tokens
+}
+
+func hackyNestedGet(data map[string]interface{}, path []string) (interface{}, 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 {
+ val, ok = hackyNestedGet(mapVal, path[1:])
+ if ok {
+ return val, true
+ }
+ }
+ }
+ // If we don't find the key, try to combine the first two parts.
+ // e.g. if the key is content.m.relates_to.rel_type, we'll first try data["m"], which will fail,
+ // then combine m and relates_to to get data["m.relates_to"], which should succeed.
+ path[1] = path[0] + "." + path[1]
+ return hackyNestedGet(data, path[1:])
+}
+
+func stringifyForPushCondition(val interface{}) string {
+ // Implement MSC3862 to allow matching any type of field
+ // https://github.com/matrix-org/matrix-spec-proposals/pull/3862
+ switch typedVal := val.(type) {
+ case string:
+ return typedVal
+ case nil:
+ return "null"
+ case float64:
+ // Floats aren't allowed in Matrix events, but the JSON parser always stores numbers as floats,
+ // so just handle that and convert to int
+ return strconv.FormatInt(int64(typedVal), 10)
+ default:
+ return fmt.Sprint(val)
+ }
+}
+
+func (cond *PushCondition) matchValue(room Room, evt *event.Event) 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())
+ case "sender":
+ return pattern.MatchString(string(evt.Sender))
+ case "room_id":
+ return pattern.MatchString(string(evt.RoomID))
+ case "state_key":
+ if evt.StateKey == nil {
+ return false
+ }
+ return pattern.MatchString(*evt.StateKey)
+ 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 pattern.MatchString(stringifyForPushCondition(val))
+ default:
+ return false
+ }
+}
+
+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.Raw), &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 {
+ return false
+ }
+
+ msg, ok := evt.Content.Raw["body"].(string)
+ if !ok {
+ return false
+ }
+
+ isAcceptable := func(r uint8) bool {
+ return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r))
+ }
+ length := len(displayname)
+ for index := strings.Index(msg, displayname); index != -1; index = strings.Index(msg, displayname) {
+ if (index <= 0 || isAcceptable(msg[index-1])) && (index+length >= len(msg) || isAcceptable(msg[index+length])) {
+ return true
+ }
+ msg = msg[index+len(displayname):]
+ }
+ return false
+}
+
+func (cond *PushCondition) matchMemberCount(room Room) bool {
+ group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition)
+ if len(group) != 3 {
+ return false
+ }
+
+ operator := group[1]
+ wantedMemberCount, _ := strconv.Atoi(group[2])
+
+ memberCount := room.GetMemberCount()
+
+ switch operator {
+ case "==", "":
+ return memberCount == wantedMemberCount
+ case ">":
+ return memberCount > wantedMemberCount
+ case ">=":
+ return memberCount >= wantedMemberCount
+ case "<":
+ return memberCount < wantedMemberCount
+ case "<=":
+ return memberCount <= wantedMemberCount
+ default:
+ // Should be impossible due to regex.
+ return false
+ }
+}