business: add privacy-aware user projection

(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
This commit is contained in:
A 2026-06-08 01:07:21 +08:00
parent 14d220c971
commit 8ff3343ae0
29 changed files with 2239 additions and 112 deletions

View file

@ -0,0 +1,125 @@
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) 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 {
raw, err := json.Marshal(rules.Rules)
if err != nil {
return err
}
_, err = s.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
}
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
}