package notifications import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // KindSetting is one row of a user's notification settings. type KindSetting struct { Kind Kind `json:"kind"` AdminOnly bool `json:"admin_only"` Inbox bool `json:"inbox"` Phone bool `json:"phone"` Email bool `json:"email"` } // SettingChange changes one kind. A nil field is left as it is, so a client // replaying a queued toggle offline sends only what the user touched. type SettingChange struct { Kind Kind `json:"kind"` Inbox *bool `json:"inbox,omitempty"` Phone *bool `json:"phone,omitempty"` Email *bool `json:"email,omitempty"` } // ErrSettingInvalid is a change naming an unknown kind, or an admin kind for // a user who is not an admin. var ErrSettingInvalid = errors.New("notification setting invalid") // LoadSettings returns every kind the user can receive, in display order, // with their stored channels or the kind's defaults. Admin kinds are listed // only for admins. // // The stored values are returned as set, not as Effective: a user who turns // the inbox off and on again gets back the phone and email choices they had. func LoadSettings(ctx context.Context, q *dbq.Queries, userID pgtype.UUID, isAdmin bool) ([]KindSetting, error) { rows, err := q.ListNotificationPrefsForUser(ctx, userID) if err != nil { return nil, fmt.Errorf("notifications: load settings: %w", err) } stored := make(map[Kind]Channels, len(rows)) for _, r := range rows { stored[Kind(r.Kind)] = Channels{Inbox: r.Inbox, Phone: r.Phone, Email: r.Email} } out := make([]KindSetting, 0, len(order)) for _, k := range order { if k.AdminOnly() && !isAdmin { continue } c, ok := stored[k] if !ok { c = k.Defaults() } out = append(out, KindSetting{Kind: k, AdminOnly: k.AdminOnly(), Inbox: c.Inbox, Phone: c.Phone, Email: c.Email}) } return out, nil } // SaveSettings applies changes and returns the settings as they now stand. // The whole batch is validated before anything is written. func SaveSettings(ctx context.Context, q *dbq.Queries, userID pgtype.UUID, isAdmin bool, changes []SettingChange) ([]KindSetting, error) { for _, c := range changes { if !c.Kind.Valid() { return nil, fmt.Errorf("%w: unknown kind %q", ErrSettingInvalid, c.Kind) } if c.Kind.AdminOnly() && !isAdmin { return nil, fmt.Errorf("%w: %s is for admins", ErrSettingInvalid, c.Kind) } } current, err := LoadSettings(ctx, q, userID, isAdmin) if err != nil { return nil, err } byKind := make(map[Kind]KindSetting, len(current)) for _, s := range current { byKind[s.Kind] = s } for _, c := range changes { s := byKind[c.Kind] if c.Inbox != nil { s.Inbox = *c.Inbox } if c.Phone != nil { s.Phone = *c.Phone } if c.Email != nil { s.Email = *c.Email } byKind[c.Kind] = s if err := q.UpsertNotificationPref(ctx, dbq.UpsertNotificationPrefParams{ UserID: userID, Kind: string(c.Kind), Inbox: s.Inbox, Phone: s.Phone, Email: s.Email, }); err != nil { return nil, fmt.Errorf("notifications: save %s: %w", c.Kind, err) } } return LoadSettings(ctx, q, userID, isAdmin) }