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>
168 lines
5.4 KiB
Go
168 lines
5.4 KiB
Go
package pushserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/pushrules"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
)
|
|
|
|
// AccountDataType is the account data push rules are kept in, and the
|
|
// type a client is refused when it tries to write them directly.
|
|
const AccountDataType = "m.push_rules"
|
|
|
|
// Push rules are the m.push_rules account data, and this is the only
|
|
// writer of it.
|
|
//
|
|
// That is where the spec has a client read them — every client takes its
|
|
// rules from the account data of its initial sync — so keeping them
|
|
// anywhere else would mean a second copy, rendered into account data on
|
|
// every change. What is stored is the whole ruleset as a client sees it.
|
|
// What is read back from it is only what the user owns: their own rules,
|
|
// and the enabled flag and actions of each default rule. The rest comes
|
|
// from pushrules.Defaults, so a server upgrade that adds a default rule
|
|
// reaches every user without rewriting anybody's account data.
|
|
|
|
// Ruleset returns a user's rules as they apply now: their own laid over
|
|
// the server defaults.
|
|
func (a *API) Ruleset(ctx context.Context, localpart, userID string) (pushrules.Ruleset, error) {
|
|
a.rulesMu.Lock()
|
|
cached, ok := a.rulesCache[localpart]
|
|
a.rulesMu.Unlock()
|
|
if ok {
|
|
return cached.Clone(), nil
|
|
}
|
|
|
|
rs, err := a.loadRuleset(ctx, localpart, userID)
|
|
if err != nil {
|
|
return pushrules.Ruleset{}, err
|
|
}
|
|
a.rulesMu.Lock()
|
|
a.rulesCache[localpart] = rs
|
|
a.rulesMu.Unlock()
|
|
return rs.Clone(), nil
|
|
}
|
|
|
|
func (a *API) loadRuleset(ctx context.Context, localpart, userID string) (pushrules.Ruleset, error) {
|
|
defaults := pushrules.Defaults(userID)
|
|
raw, err := a.accounts.AccountData(ctx, localpart, "", AccountDataType)
|
|
switch {
|
|
case errors.Is(err, storage.ErrNoSuchAccountData):
|
|
return defaults, nil
|
|
case err != nil:
|
|
return pushrules.Ruleset{}, err
|
|
}
|
|
stored, err := pushrules.ParseDocument(raw)
|
|
if err != nil {
|
|
// Only this package writes the rules, so an unreadable copy is a
|
|
// bug or a hand edit. The defaults are a better answer than
|
|
// refusing every push and every sync until somebody fixes it.
|
|
a.log.Error("stored push rules are unreadable; using the defaults",
|
|
"user_id", userID, "error", err)
|
|
return defaults, nil
|
|
}
|
|
return pushrules.Merge(stored, defaults), nil
|
|
}
|
|
|
|
// Document is a user's rules as m.push_rules content: what a sync and a
|
|
// read of the account data return.
|
|
func (a *API) Document(ctx context.Context, localpart, userID string) ([]byte, error) {
|
|
rs, err := a.Ruleset(ctx, localpart, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pushrules.MarshalDocument(rs)
|
|
}
|
|
|
|
// Rule returns one rule.
|
|
func (a *API) Rule(ctx context.Context, localpart, userID string, kind pushrules.Kind, ruleID string) (pushrules.Rule, error) {
|
|
rs, err := a.Ruleset(ctx, localpart, userID)
|
|
if err != nil {
|
|
return pushrules.Rule{}, err
|
|
}
|
|
rule, ok := rs.Find(kind, ruleID)
|
|
if !ok {
|
|
return pushrules.Rule{}, ErrNoSuchRule
|
|
}
|
|
return rule, nil
|
|
}
|
|
|
|
// PutRule creates or replaces a user rule; see pushrules.Ruleset.Put for
|
|
// where it goes.
|
|
func (a *API) PutRule(ctx context.Context, localpart, userID string, kind pushrules.Kind, ruleID string, rule pushrules.NewRule, before, after string) error {
|
|
built, err := rule.Build(kind, ruleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.editRules(ctx, localpart, userID, func(rs *pushrules.Ruleset) error {
|
|
return rs.Put(kind, built, before, after)
|
|
})
|
|
}
|
|
|
|
// DeleteRule removes a user rule.
|
|
func (a *API) DeleteRule(ctx context.Context, localpart, userID string, kind pushrules.Kind, ruleID string) error {
|
|
return a.editRules(ctx, localpart, userID, func(rs *pushrules.Ruleset) error {
|
|
return rs.Delete(kind, ruleID)
|
|
})
|
|
}
|
|
|
|
// SetRuleEnabled switches a rule on or off.
|
|
func (a *API) SetRuleEnabled(ctx context.Context, localpart, userID string, kind pushrules.Kind, ruleID string, enabled bool) error {
|
|
return a.editRules(ctx, localpart, userID, func(rs *pushrules.Ruleset) error {
|
|
return rs.SetEnabled(kind, ruleID, enabled)
|
|
})
|
|
}
|
|
|
|
// SetRuleActions replaces a rule's actions.
|
|
func (a *API) SetRuleActions(ctx context.Context, localpart, userID string, kind pushrules.Kind, ruleID string, actions []json.RawMessage) error {
|
|
return a.editRules(ctx, localpart, userID, func(rs *pushrules.Ruleset) error {
|
|
return rs.SetActions(kind, ruleID, actions)
|
|
})
|
|
}
|
|
|
|
// editRules is the read-modify-write every change is, serialised per user
|
|
// so that two changes made at once cannot each start from the ruleset the
|
|
// other has not written yet.
|
|
func (a *API) editRules(ctx context.Context, localpart, userID string, edit func(*pushrules.Ruleset) error) error {
|
|
mu := a.writeLock(localpart)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
rs, err := a.Ruleset(ctx, localpart, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := edit(&rs); err != nil {
|
|
return err
|
|
}
|
|
doc, err := pushrules.MarshalDocument(rs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := a.accounts.SetAccountData(ctx, localpart, "", AccountDataType, doc); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.rulesMu.Lock()
|
|
a.rulesCache[localpart] = rs
|
|
a.rulesMu.Unlock()
|
|
|
|
// The rules travel as account data, so a change is something every
|
|
// one of the user's syncs has to hear about now rather than at its
|
|
// timeout.
|
|
a.notifyUsers(userID)
|
|
return nil
|
|
}
|
|
|
|
func (a *API) writeLock(localpart string) *sync.Mutex {
|
|
actual, _ := a.writeLocks.LoadOrStore(localpart, &sync.Mutex{})
|
|
mu, ok := actual.(*sync.Mutex)
|
|
if !ok {
|
|
panic("pushserver: write lock map holds something that is not a mutex")
|
|
}
|
|
return mu
|
|
}
|