summary refs log tree commit diff
path: root/vendor/maunium.net/go/mautrix/pushrules
diff options
context:
space:
mode:
authorEmile <git@emile.space>2024-10-25 15:55:50 +0200
committerEmile <git@emile.space>2024-10-25 15:55:50 +0200
commitc90f36e3dd179d2de96f4f5fe38d8dc9a9de6dfe (patch)
tree89e9afb41c5bf76f48cfb09305a2d3db8d302b06 /vendor/maunium.net/go/mautrix/pushrules
parent98bbb0f559a8883bc47bae80607dbe326a448e61 (diff)
vendor HEAD main
Diffstat (limited to 'vendor/maunium.net/go/mautrix/pushrules')
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/action.go125
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/condition.go336
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/doc.go2
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/pushrules.go37
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/rule.go177
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/ruleset.go97
6 files changed, 774 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/pushrules/action.go b/vendor/maunium.net/go/mautrix/pushrules/action.go
new file mode 100644
index 0000000..9838e88
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/action.go
@@ -0,0 +1,125 @@
+// Copyright (c) 2020 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"
+
+// PushActionType is the type of a PushAction
+type PushActionType string
+
+// The allowed push action types as specified in spec section 11.12.1.4.1.
+const (
+	ActionNotify     PushActionType = "notify"
+	ActionDontNotify PushActionType = "dont_notify"
+	ActionCoalesce   PushActionType = "coalesce"
+	ActionSetTweak   PushActionType = "set_tweak"
+)
+
+// PushActionTweak is the type of the tweak in SetTweak push actions.
+type PushActionTweak string
+
+// The allowed tweak types as specified in spec section 11.12.1.4.1.1.
+const (
+	TweakSound     PushActionTweak = "sound"
+	TweakHighlight PushActionTweak = "highlight"
+)
+
+// PushActionArray is an array of PushActions.
+type PushActionArray []*PushAction
+
+// PushActionArrayShould contains the important information parsed from a PushActionArray.
+type PushActionArrayShould struct {
+	// Whether the array contained a Notify, DontNotify or Coalesce action type.
+	// Deprecated: an empty array should be treated as no notification, so there's no reason to check this field.
+	NotifySpecified bool
+	// Whether the event in question should trigger a notification.
+	Notify bool
+	// Whether the event in question should be highlighted.
+	Highlight bool
+
+	// Whether the event in question should trigger a sound alert.
+	PlaySound bool
+	// The name of the sound to play if PlaySound is true.
+	SoundName string
+}
+
+// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct.
+func (actions PushActionArray) Should() (should PushActionArrayShould) {
+	for _, action := range actions {
+		switch action.Action {
+		case ActionNotify, ActionCoalesce:
+			should.Notify = true
+			should.NotifySpecified = true
+		case ActionDontNotify:
+			should.Notify = false
+			should.NotifySpecified = true
+		case ActionSetTweak:
+			switch action.Tweak {
+			case TweakHighlight:
+				var ok bool
+				should.Highlight, ok = action.Value.(bool)
+				if !ok {
+					// Highlight value not specified, so assume true since the tweak is set.
+					should.Highlight = true
+				}
+			case TweakSound:
+				should.SoundName = action.Value.(string)
+				should.PlaySound = len(should.SoundName) > 0
+			}
+		}
+	}
+	return
+}
+
+// PushAction is a single action that should be triggered when receiving a message.
+type PushAction struct {
+	Action PushActionType
+	Tweak  PushActionTweak
+	Value  interface{}
+}
+
+// UnmarshalJSON parses JSON into this PushAction.
+//
+//   - If the JSON is a single string, the value is stored in the Action field.
+//   - If the JSON is an object with the set_tweak field, Action will be set to
+//     "set_tweak", Tweak will be set to the value of the set_tweak field and
+//     and Value will be set to the value of the value field.
+//   - In any other case, the function does nothing.
+func (action *PushAction) UnmarshalJSON(raw []byte) error {
+	var data interface{}
+
+	err := json.Unmarshal(raw, &data)
+	if err != nil {
+		return err
+	}
+
+	switch val := data.(type) {
+	case string:
+		action.Action = PushActionType(val)
+	case map[string]interface{}:
+		tweak, ok := val["set_tweak"].(string)
+		if ok {
+			action.Action = ActionSetTweak
+			action.Tweak = PushActionTweak(tweak)
+			action.Value, _ = val["value"]
+		}
+	}
+	return nil
+}
+
+// MarshalJSON is the reverse of UnmarshalJSON()
+func (action *PushAction) MarshalJSON() (raw []byte, err error) {
+	if action.Action == ActionSetTweak {
+		data := map[string]interface{}{
+			"set_tweak": action.Tweak,
+			"value":     action.Value,
+		}
+		return json.Marshal(&data)
+	}
+	data := string(action.Action)
+	return json.Marshal(&data)
+}
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 0000000..dbe83a6
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/condition.go
@@ -0,0 +1,336 @@
+// 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
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+
+	"github.com/tidwall/gjson"
+	"go.mau.fi/util/glob"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+// 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"
+	KindEventPropertyIs       PushCondKind = "event_property_is"
+	KindEventPropertyContains PushCondKind = "event_property_contains"
+
+	// 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 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"`
+
+	// 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, KindEventPropertyIs, KindEventPropertyContains:
+		return cond.matchValue(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]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]any); 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) getValue(evt *event.Event) (any, bool) {
+	key, subkey, _ := strings.Cut(cond.Key, ".")
+
+	switch key {
+	case "type":
+		return evt.Type.Type, true
+	case "sender":
+		return evt.Sender.String(), true
+	case "room_id":
+		return evt.RoomID.String(), true
+	case "state_key":
+		if evt.StateKey == nil {
+			return nil, false
+		}
+		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
+		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 := glob.CompileWithImplicitContains(cond.Pattern)
+		if pattern == nil {
+			return false
+		}
+		return pattern.Match(stringifyForPushCondition(val))
+	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))
+	}
+}
+
+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(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
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/doc.go b/vendor/maunium.net/go/mautrix/pushrules/doc.go
new file mode 100644
index 0000000..19cd774
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/doc.go
@@ -0,0 +1,2 @@
+// Package pushrules contains utilities to parse push notification rules.
+package pushrules
diff --git a/vendor/maunium.net/go/mautrix/pushrules/pushrules.go b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go
new file mode 100644
index 0000000..7944299
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2020 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/gob"
+	"encoding/json"
+	"reflect"
+
+	"maunium.net/go/mautrix/event"
+)
+
+// EventContent represents the content of a m.push_rules account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mpush_rules
+type EventContent struct {
+	Ruleset *PushRuleset `json:"global"`
+}
+
+func init() {
+	event.TypeMap[event.AccountDataPushRules] = reflect.TypeOf(EventContent{})
+	gob.Register(&EventContent{})
+}
+
+// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON.
+func EventToPushRules(evt *event.Event) (*PushRuleset, error) {
+	content := &EventContent{}
+	err := json.Unmarshal(evt.Content.VeryRaw, content)
+	if err != nil {
+		return nil, err
+	}
+
+	return content.Ruleset, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/rule.go b/vendor/maunium.net/go/mautrix/pushrules/rule.go
new file mode 100644
index 0000000..ee6d33c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/rule.go
@@ -0,0 +1,177 @@
+// Copyright (c) 2020 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/gob"
+
+	"go.mau.fi/util/glob"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+func init() {
+	gob.Register(PushRuleArray{})
+	gob.Register(PushRuleMap{})
+}
+
+type PushRuleCollection interface {
+	GetMatchingRule(room Room, evt *event.Event) *PushRule
+	GetActions(room Room, evt *event.Event) PushActionArray
+}
+
+type PushRuleArray []*PushRule
+
+func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray {
+	for _, rule := range rules {
+		rule.Type = typ
+	}
+	return rules
+}
+
+func (rules PushRuleArray) GetMatchingRule(room Room, evt *event.Event) *PushRule {
+	for _, rule := range rules {
+		if !rule.Match(room, evt) {
+			continue
+		}
+		return rule
+	}
+	return nil
+}
+
+func (rules PushRuleArray) GetActions(room Room, evt *event.Event) PushActionArray {
+	return rules.GetMatchingRule(room, evt).GetActions()
+}
+
+type PushRuleMap struct {
+	Map  map[string]*PushRule
+	Type PushRuleType
+}
+
+func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap {
+	data := PushRuleMap{
+		Map:  make(map[string]*PushRule),
+		Type: typ,
+	}
+	for _, rule := range rules {
+		rule.Type = typ
+		data.Map[rule.RuleID] = rule
+	}
+	return data
+}
+
+func (ruleMap PushRuleMap) GetMatchingRule(room Room, evt *event.Event) *PushRule {
+	var rule *PushRule
+	var found bool
+	switch ruleMap.Type {
+	case RoomRule:
+		rule, found = ruleMap.Map[string(evt.RoomID)]
+	case SenderRule:
+		rule, found = ruleMap.Map[string(evt.Sender)]
+	}
+	if found && rule.Match(room, evt) {
+		return rule
+	}
+	return nil
+}
+
+func (ruleMap PushRuleMap) GetActions(room Room, evt *event.Event) PushActionArray {
+	return ruleMap.GetMatchingRule(room, evt).GetActions()
+}
+
+func (ruleMap PushRuleMap) Unmap() PushRuleArray {
+	array := make(PushRuleArray, len(ruleMap.Map))
+	index := 0
+	for _, rule := range ruleMap.Map {
+		array[index] = rule
+		index++
+	}
+	return array
+}
+
+type PushRuleType string
+
+const (
+	OverrideRule  PushRuleType = "override"
+	ContentRule   PushRuleType = "content"
+	RoomRule      PushRuleType = "room"
+	SenderRule    PushRuleType = "sender"
+	UnderrideRule PushRuleType = "underride"
+)
+
+type PushRule struct {
+	// The type of this rule.
+	Type PushRuleType `json:"-"`
+	// The ID of this rule.
+	// For room-specific rules and user-specific rules, this is the room or user ID (respectively)
+	// For other types of rules, this doesn't affect anything.
+	RuleID string `json:"rule_id"`
+	// The actions this rule should trigger when matched.
+	Actions PushActionArray `json:"actions"`
+	// Whether this is a default rule, or has been set explicitly.
+	Default bool `json:"default"`
+	// Whether or not this push rule is enabled.
+	Enabled bool `json:"enabled"`
+	// The conditions to match in order to trigger this rule.
+	// Only applicable to generic underride/override rules.
+	Conditions []*PushCondition `json:"conditions,omitempty"`
+	// Pattern for content-specific push rules
+	Pattern string `json:"pattern,omitempty"`
+}
+
+func (rule *PushRule) GetActions() PushActionArray {
+	if rule == nil {
+		return nil
+	}
+	return rule.Actions
+}
+
+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)
+	case ContentRule:
+		return rule.matchPattern(room, evt)
+	case RoomRule:
+		return id.RoomID(rule.RuleID) == evt.RoomID
+	case SenderRule:
+		return id.UserID(rule.RuleID) == evt.Sender
+	default:
+		return false
+	}
+}
+
+func (rule *PushRule) matchConditions(room Room, evt *event.Event) bool {
+	for _, cond := range rule.Conditions {
+		if !cond.Match(room, evt) {
+			return false
+		}
+	}
+	return true
+}
+
+func (rule *PushRule) matchPattern(room Room, evt *event.Event) bool {
+	pattern := glob.CompileWithImplicitContains(rule.Pattern)
+	if pattern == nil {
+		return false
+	}
+	msg, ok := evt.Content.Raw["body"].(string)
+	if !ok {
+		return false
+	}
+	return pattern.Match(msg)
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/ruleset.go b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go
new file mode 100644
index 0000000..609997b
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go
@@ -0,0 +1,97 @@
+// Copyright (c) 2020 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"
+
+	"maunium.net/go/mautrix/event"
+)
+
+type PushRuleset struct {
+	Override  PushRuleArray
+	Content   PushRuleArray
+	Room      PushRuleMap
+	Sender    PushRuleMap
+	Underride PushRuleArray
+}
+
+type rawPushRuleset struct {
+	Override  PushRuleArray `json:"override"`
+	Content   PushRuleArray `json:"content"`
+	Room      PushRuleArray `json:"room"`
+	Sender    PushRuleArray `json:"sender"`
+	Underride PushRuleArray `json:"underride"`
+}
+
+// UnmarshalJSON parses JSON into this PushRuleset.
+//
+// For override, sender and underride push rule arrays, the type is added
+// to each PushRule and the array is used as-is.
+//
+// For room and sender push rule arrays, the type is added to each PushRule
+// and the array is converted to a map with the rule ID as the key and the
+// PushRule as the value.
+func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) {
+	data := rawPushRuleset{}
+	err = json.Unmarshal(raw, &data)
+	if err != nil {
+		return
+	}
+
+	rs.Override = data.Override.SetType(OverrideRule)
+	rs.Content = data.Content.SetType(ContentRule)
+	rs.Room = data.Room.SetTypeAndMap(RoomRule)
+	rs.Sender = data.Sender.SetTypeAndMap(SenderRule)
+	rs.Underride = data.Underride.SetType(UnderrideRule)
+	return
+}
+
+// MarshalJSON is the reverse of UnmarshalJSON()
+func (rs *PushRuleset) MarshalJSON() ([]byte, error) {
+	data := rawPushRuleset{
+		Override:  rs.Override,
+		Content:   rs.Content,
+		Room:      rs.Room.Unmap(),
+		Sender:    rs.Sender.Unmap(),
+		Underride: rs.Underride,
+	}
+	return json.Marshal(&data)
+}
+
+// DefaultPushActions is the value returned if none of the rule
+// collections in a Ruleset match the event given to GetActions()
+var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}}
+
+func (rs *PushRuleset) GetMatchingRule(room Room, evt *event.Event) (rule *PushRule) {
+	// Add push rule collections to array in priority order
+	arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride}
+	// Loop until one of the push rule collections matches the room/event combo.
+	for _, pra := range arrays {
+		if pra == nil {
+			continue
+		}
+		if rule = pra.GetMatchingRule(room, evt); rule != nil {
+			// Match found, return it.
+			return
+		}
+	}
+	// No match found
+	return nil
+}
+
+// GetActions matches the given event against all of the push rule
+// collections in this push ruleset in the order of priority as
+// specified in spec section 11.12.1.4.
+func (rs *PushRuleset) GetActions(room Room, evt *event.Event) (match PushActionArray) {
+	actions := rs.GetMatchingRule(room, evt).GetActions()
+	if actions == nil {
+		// No match found, return default actions.
+		return DefaultPushActions
+	}
+	return actions
+}