package pushrules import ( "bytes" "encoding/json" "errors" "fmt" "strings" ) // The action strings. dont_notify and coalesce are legacy: the spec has // servers accept them and treat them as doing nothing, which for // dont_notify is exactly what an empty action list means. const ( ActionNotify = "notify" ActionDontNotify = "dont_notify" ActionCoalesce = "coalesce" ) // The tweaks the server itself reads. Every other tweak goes to the push // gateway as it was written. const ( TweakHighlight = "highlight" TweakSound = "sound" ) // Outcome is what a matched rule's actions ask for. type Outcome struct { Notify bool Highlight bool // Tweaks are every set_tweak of the actions, by name, as the push // gateway is to be sent them. A highlight with no value is written // out as true. Tweaks map[string]json.RawMessage } // ParseActions reads a rule's actions. // // A highlight tweak with no value means true, as the spec says. A // highlight on a rule that does not notify is ignored: a count of // highlighted notifications that included events that are not // notifications at all would be no use to anybody. func ParseActions(actions []json.RawMessage) Outcome { out := Outcome{Tweaks: map[string]json.RawMessage{}} for _, raw := range actions { var s string if json.Unmarshal(raw, &s) == nil { if s == ActionNotify { out.Notify = true } continue } var tweak struct { SetTweak string `json:"set_tweak"` Value json.RawMessage `json:"value"` } if json.Unmarshal(raw, &tweak) != nil || tweak.SetTweak == "" { continue } value := tweak.Value if tweak.SetTweak == TweakHighlight { on := len(value) == 0 || bytes.Equal(bytes.TrimSpace(value), []byte("true")) out.Highlight = on value = json.RawMessage(fmt.Sprint(on)) } if len(value) == 0 { continue } out.Tweaks[tweak.SetTweak] = value } if !out.Notify { out.Highlight = false } return out } // ErrInvalidRule is a rule or an action list a client may not store. var ErrInvalidRule = errors.New("invalid push rule") func invalid(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrInvalidRule, fmt.Sprintf(format, args...)) } // ValidateActions checks an action list a client sent. func ValidateActions(actions []json.RawMessage) error { if actions == nil { return invalid("actions are required") } for _, raw := range actions { var s string if json.Unmarshal(raw, &s) == nil { switch s { case ActionNotify, ActionDontNotify, ActionCoalesce: continue } return invalid("%q is not an action", s) } var tweak map[string]json.RawMessage if json.Unmarshal(raw, &tweak) != nil { return invalid("an action must be a string or an object") } var name string if json.Unmarshal(tweak["set_tweak"], &name) != nil || name == "" { return invalid("an object action must name a tweak in set_tweak") } } return nil } // NewRule is a rule a client asked to create or replace, before it has // been given its kind's shape. type NewRule struct { Actions []json.RawMessage `json:"actions"` Conditions *[]Condition `json:"conditions"` Pattern *string `json:"pattern"` } // Build checks a client's rule against its kind and returns the rule as // stored. It is enabled: the spec has a new rule start that way. func (n NewRule) Build(kind Kind, ruleID string) (Rule, error) { switch { case ruleID == "": return Rule{}, invalid("a rule ID is required") case IsReserved(ruleID): return Rule{}, invalid("rule IDs starting with '.' are reserved for the server's own rules") case strings.ContainsAny(ruleID, "/\\"): return Rule{}, invalid("a rule ID may not contain a slash or a backslash") } if err := ValidateActions(n.Actions); err != nil { return Rule{}, err } rule := Rule{RuleID: ruleID, Enabled: true, Actions: n.Actions} switch kind { case KindOverride, KindUnderride: if n.Conditions == nil { return Rule{}, invalid("an %s rule needs conditions", kind) } for _, c := range *n.Conditions { if c.Kind == "" { return Rule{}, invalid("every condition needs a kind") } } rule.Conditions = *n.Conditions if rule.Conditions == nil { rule.Conditions = []Condition{} } case KindContent: if n.Pattern == nil { return Rule{}, invalid("a content rule needs a pattern") } rule.Pattern = n.Pattern case KindRoom, KindSender: // The rule ID is the condition. default: return Rule{}, invalid("%q is not a kind of push rule", kind) } return rule, nil }