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 }