// Package pushrules is the push rule language of the Client-Server API: // the rule model, the server-default ruleset, and the evaluation that // decides whether an event notifies a user. // // It is pure library code, like internal/matrix: no storage, no HTTP, no // rooms. The caller supplies the event and the handful of facts about the // room an evaluation needs — the user's display name in it, how many // members it has, how much power the sender holds — and gets back the rule // that matched. Where those facts come from, and what is done with the // answer, is internal/pushserver's business. package pushrules import ( "encoding/json" "fmt" "slices" "strings" ) // Kind is one of the five kinds of push rule, which are also the order // they are evaluated in. type Kind string // The kinds, as the spec spells them in paths and in a ruleset. const ( KindOverride Kind = "override" KindContent Kind = "content" KindRoom Kind = "room" KindSender Kind = "sender" KindUnderride Kind = "underride" ) // Kinds lists the kinds in evaluation order. var Kinds = []Kind{KindOverride, KindContent, KindRoom, KindSender, KindUnderride} // ParseKind reads a kind from a path segment. func ParseKind(s string) (Kind, bool) { k := Kind(s) return k, slices.Contains(Kinds, k) } // Rule is one push rule, in the shape the Client-Server API reads and // writes it. type Rule struct { RuleID string `json:"rule_id"` Default bool `json:"default"` Enabled bool `json:"enabled"` // Actions are kept as the JSON they arrived as. The spec has the // server pass unknown tweaks through to the push gateway untouched, // and a typed model would quietly drop them. Actions []json.RawMessage `json:"actions"` // Conditions are only on override and underride rules. A room or // sender rule's condition is its ID, and a content rule's is its // pattern. Conditions []Condition `json:"conditions,omitempty"` // Pattern is only on a content rule. Pattern *string `json:"pattern,omitempty"` } // MarshalJSON writes conditions whenever the rule has a list of them, an // empty one included: .m.rule.master's "conditions": [] is how a client // tells an override rule that always matches from a malformed one. func (r Rule) MarshalJSON() ([]byte, error) { type plain Rule if r.Conditions == nil || len(r.Conditions) > 0 { return json.Marshal(plain(r)) } return json.Marshal(struct { plain Conditions []Condition `json:"conditions"` }{plain: plain(r), Conditions: []Condition{}}) } // Condition is one condition of an override or underride rule. type Condition struct { Kind string `json:"kind"` // Key is the dotted path into the event for event_match, // event_property_is and event_property_contains, and the notification // key for sender_notification_permission. Key string `json:"key,omitempty"` // Pattern is event_match's glob. Pattern *string `json:"pattern,omitempty"` // Is is room_member_count's comparison, e.g. "2" or ">=10". Is string `json:"is,omitempty"` // Value is what event_property_is and event_property_contains compare // against. It is raw so that `null` is a value rather than an absence. Value json.RawMessage `json:"value,omitempty"` } // The condition kinds this server evaluates. A rule with any other kind of // condition never matches, which is what the spec asks of a server that // meets a condition it does not understand. const ( CondEventMatch = "event_match" CondEventPropertyIs = "event_property_is" CondEventPropertyContains = "event_property_contains" CondContainsDisplayName = "contains_display_name" CondRoomMemberCount = "room_member_count" CondSenderNotificationPermission = "sender_notification_permission" ) // Ruleset is a user's rules, one list per kind, each in priority order. type Ruleset struct { Override []Rule `json:"override"` Content []Rule `json:"content"` Room []Rule `json:"room"` Sender []Rule `json:"sender"` Underride []Rule `json:"underride"` } // Rules returns the list of one kind. func (rs *Ruleset) Rules(kind Kind) []Rule { switch kind { case KindOverride: return rs.Override case KindContent: return rs.Content case KindRoom: return rs.Room case KindSender: return rs.Sender case KindUnderride: return rs.Underride } return nil } // SetRules replaces the list of one kind. func (rs *Ruleset) SetRules(kind Kind, rules []Rule) { switch kind { case KindOverride: rs.Override = rules case KindContent: rs.Content = rules case KindRoom: rs.Room = rules case KindSender: rs.Sender = rules case KindUnderride: rs.Underride = rules } } // Find returns a rule by kind and ID. func (rs *Ruleset) Find(kind Kind, ruleID string) (Rule, bool) { for _, r := range rs.Rules(kind) { if r.RuleID == ruleID { return r, true } } return Rule{}, false } // normalise replaces nil lists with empty ones, so that a ruleset always // marshals with all five keys as arrays: a client reading `null` where it // expects a list of rules is a client that crashes. func (rs *Ruleset) normalise() { for _, kind := range Kinds { rules := rs.Rules(kind) if rules == nil { rules = []Rule{} } for i := range rules { if rules[i].Actions == nil { rules[i].Actions = []json.RawMessage{} } } rs.SetRules(kind, rules) } } // MarshalJSON writes the ruleset with every kind present. func (rs Ruleset) MarshalJSON() ([]byte, error) { rs.normalise() type plain Ruleset return json.Marshal(plain(rs)) } // Clone returns a deep enough copy that editing one list of it leaves the // original alone. func (rs Ruleset) Clone() Ruleset { var out Ruleset for _, kind := range Kinds { out.SetRules(kind, slices.Clone(rs.Rules(kind))) } return out } // IsReserved reports whether a rule ID is in the namespace the spec keeps // for server-default rules. A client may change the actions of such a rule // and switch it on and off, but may not create, replace or delete one. func IsReserved(ruleID string) bool { return strings.HasPrefix(ruleID, ".") } // Merge lays a user's stored rules over the server defaults. // // What a user owns is the rules they made, and the enabled flag and // actions of each default rule; the default rules themselves — their // conditions, and which of them exist — are the server's, and come from // defaults. Storing the whole ruleset and re-reading only those parts is // what lets a server upgrade bring in a default rule a newer spec version // added, without a migration rewriting every user's rules. // // The order within a kind is the spec's: user rules outrank the defaults // of their kind, except that .m.rule.master stays ahead of everything so // that switching it on still silences all. func Merge(stored, defaults Ruleset) Ruleset { var out Ruleset for _, kind := range Kinds { overrides := map[string]Rule{} var user []Rule for _, r := range stored.Rules(kind) { if r.Default { overrides[r.RuleID] = r continue } if IsReserved(r.RuleID) { // A user rule cannot have a reserved ID; one that does was // not written by this server and is ignored rather than // allowed to shadow a default. continue } user = append(user, r) } var prepend, appended []Rule for _, d := range defaults.Rules(kind) { if o, ok := overrides[d.RuleID]; ok { d.Enabled = o.Enabled d.Actions = o.Actions } if d.RuleID == RuleMaster { prepend = append(prepend, d) continue } appended = append(appended, d) } rules := make([]Rule, 0, len(prepend)+len(user)+len(appended)) rules = append(rules, prepend...) rules = append(rules, user...) rules = append(rules, appended...) out.SetRules(kind, rules) } out.normalise() return out } // Document is the content of the m.push_rules account data: the ruleset // under the one scope Matrix still has. type Document struct { Global Ruleset `json:"global"` } // ParseDocument reads stored m.push_rules content. func ParseDocument(raw []byte) (Ruleset, error) { var doc Document if err := json.Unmarshal(raw, &doc); err != nil { return Ruleset{}, fmt.Errorf("pushrules: stored ruleset is not readable: %w", err) } return doc.Global, nil } // MarshalDocument renders a ruleset as m.push_rules content. func MarshalDocument(rs Ruleset) ([]byte, error) { return json.Marshal(Document{Global: rs}) }