Files
Warp/server/internal/pushrules/edit.go
T
tajniak81andClaude Opus 5.5 b507753172 Phase 8: push and notifications
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>
2026-10-17 11:43:36 +02:00

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
}