Implements PLAN.md Phase 8. internal/pushrules is the rule language: the spec's default ruleset, all six condition kinds, glob and word-boundary matching, intentional mentions switching off the legacy body rules, and rule ordering as Synapse has it. internal/pushserver keeps the rules, pushers and notifications and talks to push gateways. Push rules are the m.push_rules account data, written only by the push server; reads keep the user's own rules and the enabled/actions of each default and take the rest from the current defaults, so new default rules reach every user without a migration. PUT account_data/m.push_rules is refused with 405. The push server listens on the roomserver's emit stage and evaluates each event on its own worker for every local member and an invite's target. Notifications and per-room unread counts are stored in migration 0010; counts are a seventh sync token stream, so /sync reports unread_notifications and picks up count changes made after the event or by a receipt from another device. m.read and m.read.private receipts clear the counts; leaving a room clears it. Pushers (http kind only) push through a bounded sender pool with retries, honour event_id_only, drop push keys the gateway rejects, and go away with their device. GET /notifications pages the history. New config section push: enabled, include_content, gateway_timeout, notification_retention. Account data writes now wake the user's blocked syncs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package pushrules
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"slices"
|
|
)
|
|
|
|
// Errors an edit returns for cases a caller has to tell apart.
|
|
var (
|
|
// ErrNoSuchRule is a rule that does not exist.
|
|
ErrNoSuchRule = errors.New("no such push rule")
|
|
|
|
// ErrNoSuchAnchor is a before or after naming a rule that is not a
|
|
// user rule of the same kind.
|
|
ErrNoSuchAnchor = errors.New("the rule to place this one next to does not exist")
|
|
)
|
|
|
|
// Put creates or replaces a user rule.
|
|
//
|
|
// With before or after it goes next to that rule, which must be a user
|
|
// rule of the same kind. With neither, a new rule goes first among the
|
|
// user rules of its kind and a replaced one keeps its place and its
|
|
// enabled flag. Either way a user rule stays below .m.rule.master and above
|
|
// every other default of its kind, which is where Merge puts them.
|
|
func (rs *Ruleset) Put(kind Kind, rule Rule, before, after string) error {
|
|
if IsReserved(rule.RuleID) {
|
|
return invalid("rule IDs starting with '.' are reserved for the server's own rules")
|
|
}
|
|
if IsReserved(before) || IsReserved(after) {
|
|
return ErrNoSuchAnchor
|
|
}
|
|
rules := slices.Clone(rs.Rules(kind))
|
|
|
|
existing := slices.IndexFunc(rules, func(r Rule) bool { return r.RuleID == rule.RuleID && !r.Default })
|
|
if existing >= 0 && before == "" && after == "" {
|
|
rule.Enabled = rules[existing].Enabled
|
|
rules[existing] = rule
|
|
rs.SetRules(kind, rules)
|
|
return nil
|
|
}
|
|
if existing >= 0 {
|
|
rules = slices.Delete(rules, existing, existing+1)
|
|
}
|
|
|
|
var at int
|
|
switch {
|
|
case before != "" || after != "":
|
|
anchor := before
|
|
if anchor == "" {
|
|
anchor = after
|
|
}
|
|
i := slices.IndexFunc(rules, func(r Rule) bool { return r.RuleID == anchor && !r.Default })
|
|
if i < 0 {
|
|
return ErrNoSuchAnchor
|
|
}
|
|
at = i
|
|
if before == "" {
|
|
at = i + 1
|
|
}
|
|
default:
|
|
// First among the user rules: after any default that Merge puts
|
|
// ahead of them.
|
|
at = slices.IndexFunc(rules, func(r Rule) bool { return !r.Default || r.RuleID != RuleMaster })
|
|
if at < 0 {
|
|
at = len(rules)
|
|
}
|
|
}
|
|
rs.SetRules(kind, slices.Insert(rules, at, rule))
|
|
return nil
|
|
}
|
|
|
|
// Delete removes a user rule. A default rule cannot be deleted, only
|
|
// switched off, and is reported as not there.
|
|
func (rs *Ruleset) Delete(kind Kind, ruleID string) error {
|
|
rules := rs.Rules(kind)
|
|
i := slices.IndexFunc(rules, func(r Rule) bool { return r.RuleID == ruleID && !r.Default })
|
|
if i < 0 {
|
|
return ErrNoSuchRule
|
|
}
|
|
rs.SetRules(kind, slices.Delete(slices.Clone(rules), i, i+1))
|
|
return nil
|
|
}
|
|
|
|
// SetEnabled switches a rule on or off. Default rules included.
|
|
func (rs *Ruleset) SetEnabled(kind Kind, ruleID string, enabled bool) error {
|
|
return rs.update(kind, ruleID, func(r *Rule) { r.Enabled = enabled })
|
|
}
|
|
|
|
// SetActions replaces a rule's actions. Default rules included.
|
|
func (rs *Ruleset) SetActions(kind Kind, ruleID string, actions []json.RawMessage) error {
|
|
if err := ValidateActions(actions); err != nil {
|
|
return err
|
|
}
|
|
return rs.update(kind, ruleID, func(r *Rule) { r.Actions = actions })
|
|
}
|
|
|
|
func (rs *Ruleset) update(kind Kind, ruleID string, fn func(*Rule)) error {
|
|
rules := slices.Clone(rs.Rules(kind))
|
|
i := slices.IndexFunc(rules, func(r Rule) bool { return r.RuleID == ruleID })
|
|
if i < 0 {
|
|
return ErrNoSuchRule
|
|
}
|
|
fn(&rules[i])
|
|
rs.SetRules(kind, rules)
|
|
return nil
|
|
}
|