package postgres import ( "context" "encoding/json" "errors" "fmt" "github.com/jackc/pgx/v5" "telesrv/internal/domain" "telesrv/internal/store" "telesrv/internal/store/postgres/sqlcgen" ) var _ store.PrivacyStore = (*PrivacyStore)(nil) // PrivacyStore persists account privacy rules in PostgreSQL. type PrivacyStore struct { db sqlcgen.DBTX } func NewPrivacyStore(db sqlcgen.DBTX) *PrivacyStore { return &PrivacyStore{db: db} } func (s *PrivacyStore) SupportsDurablePrivacyUpdates() bool { if s == nil { return false } _, ok := s.db.(interface { Begin(context.Context) (pgx.Tx, error) }) return ok } func (s *PrivacyStore) GetPrivacyRules(ctx context.Context, ownerUserID int64, key domain.PrivacyKey) (domain.PrivacyRules, bool, error) { row := s.db.QueryRow(ctx, ` SELECT rules::text FROM account_privacy_rules WHERE owner_user_id = $1 AND privacy_key = $2 `, ownerUserID, string(key)) var raw string if err := row.Scan(&raw); err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.PrivacyRules{}, false, nil } return domain.PrivacyRules{}, false, fmt.Errorf("get privacy rules: %w", err) } rules, err := decodePrivacyRulesJSON(raw) if err != nil { return domain.PrivacyRules{}, false, err } return domain.PrivacyRules{OwnerUserID: ownerUserID, Key: key, Rules: rules}, true, nil } func (s *PrivacyStore) SetPrivacyRules(ctx context.Context, rules domain.PrivacyRules) error { return setPrivacyRules(ctx, s.db, rules) } func setPrivacyRules(ctx context.Context, db sqlcgen.DBTX, rules domain.PrivacyRules) error { raw, err := json.Marshal(rules.Rules) if err != nil { return err } _, err = db.Exec(ctx, ` INSERT INTO account_privacy_rules (owner_user_id, privacy_key, rules, updated_at) VALUES ($1, $2, $3::jsonb, NOW()) ON CONFLICT (owner_user_id, privacy_key) DO UPDATE SET rules = EXCLUDED.rules, updated_at = EXCLUDED.updated_at `, rules.OwnerUserID, string(rules.Key), string(raw)) if err != nil { return fmt.Errorf("set privacy rules: %w", err) } return nil } // SetPrivacyRulesWithUpdate commits the mutable rule row and the immutable // account update snapshot in one transaction. A privacy rule can therefore // never become visible without a matching pts event/outbox item. func (s *PrivacyStore) SetPrivacyRulesWithUpdate( ctx context.Context, rules domain.PrivacyRules, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64, ) (domain.UpdateEvent, error) { beginner, ok := s.db.(interface { Begin(context.Context) (pgx.Tx, error) }) if !ok { return domain.UpdateEvent{}, fmt.Errorf("privacy update transaction unavailable") } tx, err := beginner.Begin(ctx) if err != nil { return domain.UpdateEvent{}, fmt.Errorf("begin privacy update: %w", err) } committed := false defer func() { if !committed { _ = tx.Rollback(ctx) } }() if err := setPrivacyRules(ctx, tx, rules); err != nil { return domain.UpdateEvent{}, err } if event.Date == 0 { return domain.UpdateEvent{}, fmt.Errorf("privacy update date is required") } event.Type = domain.UpdateEventPrivacy event.Privacy = rules event.PtsCount = 1 qtx := sqlcgen.New(tx) recorded, err := NewUpdateEventStore(tx).appendInTx( ctx, tx, qtx, rules.OwnerUserID, event, true, excludeAuthKeyID, excludeSessionID, true, ) if err != nil { return domain.UpdateEvent{}, fmt.Errorf("append privacy update: %w", err) } if err := tx.Commit(ctx); err != nil { return domain.UpdateEvent{}, fmt.Errorf("commit privacy update: %w", err) } committed = true return recorded, nil } func (s *PrivacyStore) ListPrivacyRules(ctx context.Context, ownerUserIDs []int64, keys []domain.PrivacyKey) ([]domain.PrivacyRules, error) { if len(ownerUserIDs) == 0 || len(keys) == 0 { return nil, nil } rows, err := s.db.Query(ctx, ` SELECT owner_user_id, privacy_key, rules::text FROM account_privacy_rules WHERE owner_user_id = ANY($1::bigint[]) AND privacy_key = ANY($2::text[]) `, ownerUserIDs, privacyKeyStrings(keys)) if err != nil { return nil, fmt.Errorf("list privacy rules: %w", err) } defer rows.Close() out := make([]domain.PrivacyRules, 0) for rows.Next() { var ownerUserID int64 var key string var raw string if err := rows.Scan(&ownerUserID, &key, &raw); err != nil { return nil, err } rules, err := decodePrivacyRulesJSON(raw) if err != nil { return nil, err } out = append(out, domain.PrivacyRules{ OwnerUserID: ownerUserID, Key: domain.PrivacyKey(key), Rules: rules, }) } if err := rows.Err(); err != nil { return nil, err } return out, nil } func privacyKeyStrings(keys []domain.PrivacyKey) []string { out := make([]string, 0, len(keys)) for _, key := range keys { out = append(out, string(key)) } return out } func decodePrivacyRulesJSON(raw string) ([]domain.PrivacyRule, error) { if raw == "" { return nil, nil } var rules []domain.PrivacyRule if err := json.Unmarshal([]byte(raw), &rules); err != nil { return nil, fmt.Errorf("decode privacy rules: %w", err) } for i := range rules { rules[i].UserIDs = append([]int64(nil), rules[i].UserIDs...) rules[i].ChatIDs = append([]int64(nil), rules[i].ChatIDs...) } return rules, nil }