business: add privacy-aware user projection
(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
This commit is contained in:
parent
14d220c971
commit
8ff3343ae0
29 changed files with 2239 additions and 112 deletions
|
|
@ -59,6 +59,98 @@ func (s *ContactStore) Get(ctx context.Context, userID, contactUserID int64) (do
|
|||
return contact, true, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) GetMany(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.Contact, error) {
|
||||
out := make(map[int64]domain.Contact, len(contactUserIDs))
|
||||
if userID == 0 || len(contactUserIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT
|
||||
c.contact_user_id,
|
||||
c.mutual,
|
||||
c.contact_phone,
|
||||
c.contact_first_name,
|
||||
c.contact_last_name,
|
||||
c.note,
|
||||
COALESCE(c.note_entities::text, '[]')::text AS note_entities_json,
|
||||
u.id,
|
||||
u.access_hash,
|
||||
COALESCE(NULLIF(c.contact_phone, ''), u.phone)::text AS phone,
|
||||
COALESCE(NULLIF(c.contact_first_name, ''), u.first_name)::text AS first_name,
|
||||
COALESCE(c.contact_last_name, u.last_name)::text AS last_name,
|
||||
u.username,
|
||||
u.country_code,
|
||||
u.verified,
|
||||
u.support,
|
||||
u.last_seen_at
|
||||
FROM contacts c
|
||||
JOIN users u ON u.id = c.contact_user_id
|
||||
WHERE c.user_id = $1
|
||||
AND c.contact_user_id = ANY($2::bigint[])
|
||||
`, userID, contactUserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get contacts many: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
contact, err := scanContactRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[contact.User.ID] = contact
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) GetReverseContacts(ctx context.Context, userID int64, ownerUserIDs []int64) (map[int64]domain.Contact, error) {
|
||||
out := make(map[int64]domain.Contact, len(ownerUserIDs))
|
||||
if userID == 0 || len(ownerUserIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT
|
||||
c.user_id AS owner_user_id,
|
||||
c.mutual,
|
||||
c.contact_phone,
|
||||
c.contact_first_name,
|
||||
c.contact_last_name,
|
||||
c.note,
|
||||
COALESCE(c.note_entities::text, '[]')::text AS note_entities_json,
|
||||
u.id,
|
||||
u.access_hash,
|
||||
COALESCE(NULLIF(c.contact_phone, ''), u.phone)::text AS phone,
|
||||
COALESCE(NULLIF(c.contact_first_name, ''), u.first_name)::text AS first_name,
|
||||
COALESCE(c.contact_last_name, u.last_name)::text AS last_name,
|
||||
u.username,
|
||||
u.country_code,
|
||||
u.verified,
|
||||
u.support,
|
||||
u.last_seen_at
|
||||
FROM contacts c
|
||||
JOIN users u ON u.id = c.contact_user_id
|
||||
WHERE c.contact_user_id = $1
|
||||
AND c.user_id = ANY($2::bigint[])
|
||||
`, userID, ownerUserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get reverse contacts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
ownerID, contact, err := scanReverseContactRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[ownerID] = contact
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) Upsert(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
|
||||
entities, err := encodeMessageEntities(input.NoteEntities)
|
||||
if err != nil {
|
||||
|
|
@ -295,6 +387,70 @@ func (s *ContactStore) UpdateNote(ctx context.Context, userID, contactUserID int
|
|||
return contact, true, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
|
||||
tag, err := s.db.Exec(ctx, `
|
||||
UPDATE contacts
|
||||
SET personal_photo_id = $3,
|
||||
personal_photo_date = CASE WHEN $3::bigint = 0 THEN 0 ELSE $4::int END,
|
||||
updated_at = now()
|
||||
WHERE user_id = $1
|
||||
AND contact_user_id = $2
|
||||
`, userID, contactUserID, photoID, date)
|
||||
if err != nil {
|
||||
return domain.Contact{}, false, fmt.Errorf("set contact personal photo: %w", err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return domain.Contact{}, false, nil
|
||||
}
|
||||
contact, found, err := s.Get(ctx, userID, contactUserID)
|
||||
return contact, found, err
|
||||
}
|
||||
|
||||
func (s *ContactStore) PersonalPhotos(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(contactUserIDs))
|
||||
if userID == 0 || len(contactUserIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT
|
||||
c.contact_user_id,
|
||||
c.personal_photo_id,
|
||||
ph.dc_id,
|
||||
ph.sizes::text AS sizes_json
|
||||
FROM contacts c
|
||||
JOIN photos ph ON ph.id = c.personal_photo_id
|
||||
WHERE c.user_id = $1
|
||||
AND c.contact_user_id = ANY($2::bigint[])
|
||||
AND c.personal_photo_id <> 0
|
||||
`, userID, contactUserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list contact personal photos: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var contactUserID, photoID int64
|
||||
var dcID int32
|
||||
var sizesJSON string
|
||||
if err := rows.Scan(&contactUserID, &photoID, &dcID, &sizesJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sizes, err := decodePhotoSizes(sizesJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[contactUserID] = domain.ProfilePhotoRef{
|
||||
PhotoID: photoID,
|
||||
DCID: int(dcID),
|
||||
Stripped: domain.StrippedFromSizes(sizes),
|
||||
Personal: true,
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) Delete(ctx context.Context, userID int64, contactUserIDs []int64) (int, error) {
|
||||
if len(contactUserIDs) == 0 {
|
||||
return 0, nil
|
||||
|
|
@ -366,6 +522,107 @@ func contactFromFields(id, accessHash int64, phone, firstName, lastName, usernam
|
|||
}
|
||||
}
|
||||
|
||||
type contactScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanContactRows(row contactScanner) (domain.Contact, error) {
|
||||
var (
|
||||
contactUserID int64
|
||||
mutual bool
|
||||
contactPhone string
|
||||
contactFirstName string
|
||||
contactLastName string
|
||||
note string
|
||||
noteEntitiesJSON string
|
||||
id int64
|
||||
accessHash int64
|
||||
phone string
|
||||
firstName string
|
||||
lastName string
|
||||
username string
|
||||
countryCode string
|
||||
verified bool
|
||||
support bool
|
||||
lastSeenAt int32
|
||||
)
|
||||
if err := row.Scan(
|
||||
&contactUserID,
|
||||
&mutual,
|
||||
&contactPhone,
|
||||
&contactFirstName,
|
||||
&contactLastName,
|
||||
¬e,
|
||||
¬eEntitiesJSON,
|
||||
&id,
|
||||
&accessHash,
|
||||
&phone,
|
||||
&firstName,
|
||||
&lastName,
|
||||
&username,
|
||||
&countryCode,
|
||||
&verified,
|
||||
&support,
|
||||
&lastSeenAt,
|
||||
); err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
entities, err := decodeMessageEntities(noteEntitiesJSON)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
return contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual), nil
|
||||
}
|
||||
|
||||
func scanReverseContactRows(row contactScanner) (int64, domain.Contact, error) {
|
||||
var (
|
||||
ownerUserID int64
|
||||
mutual bool
|
||||
contactPhone string
|
||||
contactFirstName string
|
||||
contactLastName string
|
||||
note string
|
||||
noteEntitiesJSON string
|
||||
id int64
|
||||
accessHash int64
|
||||
phone string
|
||||
firstName string
|
||||
lastName string
|
||||
username string
|
||||
countryCode string
|
||||
verified bool
|
||||
support bool
|
||||
lastSeenAt int32
|
||||
)
|
||||
if err := row.Scan(
|
||||
&ownerUserID,
|
||||
&mutual,
|
||||
&contactPhone,
|
||||
&contactFirstName,
|
||||
&contactLastName,
|
||||
¬e,
|
||||
¬eEntitiesJSON,
|
||||
&id,
|
||||
&accessHash,
|
||||
&phone,
|
||||
&firstName,
|
||||
&lastName,
|
||||
&username,
|
||||
&countryCode,
|
||||
&verified,
|
||||
&support,
|
||||
&lastSeenAt,
|
||||
); err != nil {
|
||||
return 0, domain.Contact{}, err
|
||||
}
|
||||
entities, err := decodeMessageEntities(noteEntitiesJSON)
|
||||
if err != nil {
|
||||
return 0, domain.Contact{}, err
|
||||
}
|
||||
contact := contactFromFields(id, accessHash, phone, firstName, lastName, username, countryCode, verified, support, int(lastSeenAt), contactFirstName, contactLastName, contactPhone, note, entities, mutual)
|
||||
return ownerUserID, contact, nil
|
||||
}
|
||||
|
||||
func (s *ContactStore) Block(ctx context.Context, userID, blockedUserID int64, date int) (bool, error) {
|
||||
if userID == 0 || blockedUserID == 0 || userID == blockedUserID {
|
||||
return false, nil
|
||||
|
|
|
|||
|
|
@ -420,27 +420,44 @@ func (s *MediaStore) CountAvailableReactions(ctx context.Context) (int, error) {
|
|||
// ---- 头像历史 ----
|
||||
|
||||
func (s *MediaStore) AddProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID, photoID int64, date int) error {
|
||||
next, err := s.q.NextProfilePhotoOrder(ctx, sqlcgen.NextProfilePhotoOrderParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
return s.AddProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photoID, date)
|
||||
}
|
||||
|
||||
func (s *MediaStore) AddProfilePhotoKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoID int64, date int) error {
|
||||
kind = normalizeProfilePhotoKind(kind)
|
||||
next, err := s.nextProfilePhotoOrder(ctx, ownerType, ownerID, kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.q.AddProfilePhoto(ctx, sqlcgen.AddProfilePhotoParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
PhotoID: photoID,
|
||||
Date: int32(date),
|
||||
SortOrder: next + 1,
|
||||
})
|
||||
_, err = s.db.Exec(ctx, `
|
||||
INSERT INTO profile_photos (owner_peer_type, owner_peer_id, kind, photo_id, date, active, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, true, $6)
|
||||
ON CONFLICT (owner_peer_type, owner_peer_id, kind, photo_id) DO UPDATE SET
|
||||
date = EXCLUDED.date,
|
||||
active = true,
|
||||
sort_order = EXCLUDED.sort_order
|
||||
`, string(ownerType), ownerID, string(kind), photoID, date, next+1)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID int64) (int64, bool, error) {
|
||||
id, err := s.q.CurrentProfilePhoto(ctx, sqlcgen.CurrentProfilePhotoParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
return s.CurrentProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile)
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhotoKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (int64, bool, error) {
|
||||
kind = normalizeProfilePhotoKind(kind)
|
||||
row := s.db.QueryRow(ctx, `
|
||||
SELECT photo_id
|
||||
FROM profile_photos
|
||||
WHERE owner_peer_type = $1
|
||||
AND owner_peer_id = $2
|
||||
AND kind = $3
|
||||
AND active
|
||||
ORDER BY sort_order DESC
|
||||
LIMIT 1
|
||||
`, string(ownerType), ownerID, string(kind))
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, false, nil
|
||||
|
|
@ -451,59 +468,156 @@ func (s *MediaStore) CurrentProfilePhoto(ctx context.Context, ownerType domain.P
|
|||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
return s.CurrentProfilePhotosKind(ctx, ownerType, ownerIDs, domain.ProfilePhotoKindProfile)
|
||||
}
|
||||
|
||||
func (s *MediaStore) CurrentProfilePhotosKind(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64, kind domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return map[int64]domain.ProfilePhotoRef{}, nil
|
||||
}
|
||||
rows, err := s.q.CurrentProfilePhotosForOwners(ctx, sqlcgen.CurrentProfilePhotosForOwnersParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerIds: ownerIDs,
|
||||
})
|
||||
kind = normalizeProfilePhotoKind(kind)
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT DISTINCT ON (pp.owner_peer_id)
|
||||
pp.owner_peer_id,
|
||||
pp.photo_id,
|
||||
ph.dc_id,
|
||||
ph.sizes::text AS sizes_json
|
||||
FROM profile_photos pp
|
||||
JOIN photos ph ON ph.id = pp.photo_id
|
||||
WHERE pp.owner_peer_type = $1
|
||||
AND pp.owner_peer_id = ANY($2::bigint[])
|
||||
AND pp.kind = $3
|
||||
AND pp.active
|
||||
ORDER BY pp.owner_peer_id, pp.sort_order DESC
|
||||
`, string(ownerType), ownerIDs, string(kind))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(rows))
|
||||
for _, r := range rows {
|
||||
sizes, err := decodePhotoSizes(r.SizesJson)
|
||||
defer rows.Close()
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(ownerIDs))
|
||||
for rows.Next() {
|
||||
var ownerID, photoID int64
|
||||
var dcID int32
|
||||
var sizesJSON string
|
||||
if err := rows.Scan(&ownerID, &photoID, &dcID, &sizesJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sizes, err := decodePhotoSizes(sizesJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[r.OwnerPeerID] = domain.ProfilePhotoRef{
|
||||
PhotoID: r.PhotoID,
|
||||
DCID: int(r.DcID),
|
||||
out[ownerID] = domain.ProfilePhotoRef{
|
||||
PhotoID: photoID,
|
||||
DCID: int(dcID),
|
||||
Stripped: domain.StrippedFromSizes(sizes),
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) ListProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, offset, limit int, maxID int64) ([]int64, int, error) {
|
||||
ids, err := s.q.ListProfilePhotos(ctx, sqlcgen.ListProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
MaxID: maxID,
|
||||
OffsetCount: int32(offset),
|
||||
LimitCount: int32(limit),
|
||||
})
|
||||
return s.ListProfilePhotosKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, offset, limit, maxID)
|
||||
}
|
||||
|
||||
func (s *MediaStore) ListProfilePhotosKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]int64, int, error) {
|
||||
kind = normalizeProfilePhotoKind(kind)
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT photo_id
|
||||
FROM profile_photos
|
||||
WHERE owner_peer_type = $1
|
||||
AND owner_peer_id = $2
|
||||
AND kind = $3
|
||||
AND active
|
||||
AND ($4::bigint <= 0 OR photo_id < $4::bigint)
|
||||
ORDER BY sort_order DESC
|
||||
OFFSET $5
|
||||
LIMIT $6
|
||||
`, string(ownerType), ownerID, string(kind), maxID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total, err := s.q.CountProfilePhotos(ctx, sqlcgen.CountProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
})
|
||||
defer rows.Close()
|
||||
ids := make([]int64, 0, limit)
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var total int
|
||||
err = s.db.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM profile_photos
|
||||
WHERE owner_peer_type = $1
|
||||
AND owner_peer_id = $2
|
||||
AND kind = $3
|
||||
AND active
|
||||
`, string(ownerType), ownerID, string(kind)).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ids, int(total), nil
|
||||
return ids, total, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) DeleteProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) ([]int64, error) {
|
||||
return s.DeleteProfilePhotosKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photoIDs)
|
||||
}
|
||||
|
||||
func (s *MediaStore) DeleteProfilePhotosKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoIDs []int64) ([]int64, error) {
|
||||
if len(photoIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.q.DeactivateProfilePhotos(ctx, sqlcgen.DeactivateProfilePhotosParams{
|
||||
OwnerPeerType: string(ownerType),
|
||||
OwnerPeerID: ownerID,
|
||||
PhotoIds: photoIDs,
|
||||
})
|
||||
kind = normalizeProfilePhotoKind(kind)
|
||||
rows, err := s.db.Query(ctx, `
|
||||
UPDATE profile_photos
|
||||
SET active = false
|
||||
WHERE owner_peer_type = $1
|
||||
AND owner_peer_id = $2
|
||||
AND kind = $3
|
||||
AND photo_id = ANY($4::bigint[])
|
||||
AND active
|
||||
RETURNING photo_id
|
||||
`, string(ownerType), ownerID, string(kind), photoIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
deleted := make([]int64, 0, len(photoIDs))
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deleted = append(deleted, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (s *MediaStore) nextProfilePhotoOrder(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (int64, error) {
|
||||
var maxOrder int64
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT COALESCE(MAX(sort_order), 0)::bigint
|
||||
FROM profile_photos
|
||||
WHERE owner_peer_type = $1
|
||||
AND owner_peer_id = $2
|
||||
AND kind = $3
|
||||
`, string(ownerType), ownerID, string(kind)).Scan(&maxOrder)
|
||||
return maxOrder, err
|
||||
}
|
||||
|
||||
func normalizeProfilePhotoKind(kind domain.ProfilePhotoKind) domain.ProfilePhotoKind {
|
||||
if kind == domain.ProfilePhotoKindFallback {
|
||||
return kind
|
||||
}
|
||||
return domain.ProfilePhotoKindProfile
|
||||
}
|
||||
|
|
|
|||
125
internal/store/postgres/privacy.go
Normal file
125
internal/store/postgres/privacy.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue