fix: sync contact phone privacy disclosure

This commit is contained in:
iamxvbaba 2026-07-24 11:56:58 +08:00
parent 0e2fcdf9c8
commit e1a95c7318
19 changed files with 789 additions and 50 deletions

View file

@ -138,9 +138,6 @@ func (s *ContactStore) Upsert(_ context.Context, userID int64, input domain.Cont
contact.User.EmojiStatusUntil = existing.User.EmojiStatusUntil
contact.CloseFriend = existing.CloseFriend
contact.User.CloseFriend = existing.CloseFriend || existing.User.CloseFriend
if contact.Phone == "" {
contact.User.Phone = existing.User.Phone
}
if contact.FirstName == "" {
contact.User.FirstName = existing.User.FirstName
}

View file

@ -1597,7 +1597,6 @@ func storyViewerMatchesQuery(viewerID int64, query string, profile domain.User,
profile.LastName,
strings.TrimSpace(profile.FirstName + " " + profile.LastName),
profile.Username,
profile.Phone,
strconv.FormatInt(viewerID, 10),
}
if isContact {
@ -1610,7 +1609,6 @@ func storyViewerMatchesQuery(viewerID int64, query string, profile domain.User,
contact.User.LastName,
strings.TrimSpace(contact.User.FirstName+" "+contact.User.LastName),
contact.User.Username,
contact.User.Phone,
)
}
for _, candidate := range candidates {

View file

@ -1627,6 +1627,33 @@ func TestStoryStoreListStoryViewsFiltersByContactsAndQuery(t *testing.T) {
t.Fatalf("username query = %+v, want viewer 2002", stranger)
}
hiddenAccountPhone, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "155502",
})
if err != nil {
t.Fatalf("list query hidden account phone: %v", err)
}
if hiddenAccountPhone.Count != 0 || len(hiddenAccountPhone.Views) != 0 {
t.Fatalf("hidden account phone query = %+v, want no match", hiddenAccountPhone)
}
knownContactPhone, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "7001",
})
if err != nil {
t.Fatalf("list query known contact phone: %v", err)
}
if knownContactPhone.Count != 1 || len(knownContactPhone.Views) != 1 || knownContactPhone.Views[0].ViewerID != 2001 {
t.Fatalf("known contact phone query = %+v, want viewer 2001", knownContactPhone)
}
intersection, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,

View file

@ -419,6 +419,7 @@ func (f *fakeDialogReadModelCache) flushCount() int {
type fakePrivacyReadModelCache struct {
mu sync.Mutex
ids []int64
warmed []int64
flushes int
}
@ -428,6 +429,13 @@ func (f *fakePrivacyReadModelCache) InvalidateOwners(ids ...int64) {
f.ids = append(f.ids, ids...)
}
func (f *fakePrivacyReadModelCache) WarmOwners(_ context.Context, ids ...int64) error {
f.mu.Lock()
defer f.mu.Unlock()
f.warmed = append(f.warmed, ids...)
return nil
}
func (f *fakePrivacyReadModelCache) FlushReadModelCache() {
f.mu.Lock()
defer f.mu.Unlock()
@ -440,6 +448,12 @@ func (f *fakePrivacyReadModelCache) idsSnapshot() []int64 {
return append([]int64(nil), f.ids...)
}
func (f *fakePrivacyReadModelCache) warmedSnapshot() []int64 {
f.mu.Lock()
defer f.mu.Unlock()
return append([]int64(nil), f.warmed...)
}
func (f *fakePrivacyReadModelCache) flushCount() int {
f.mu.Lock()
defer f.mu.Unlock()
@ -509,6 +523,9 @@ func TestReadModelChangeListenerInvalidatesAccountCaches(t *testing.T) {
if len(privacy.ids) != 1 || privacy.ids[0] != 21 {
t.Fatalf("privacy invalidations = %v, want [21]", privacy.ids)
}
if warmed := privacy.warmedSnapshot(); len(warmed) != 1 || warmed[0] != 21 {
t.Fatalf("privacy warms = %v, want [21]", warmed)
}
listener.handlePayload(`{"model":"dialog_light","owner_user_id":22,"peer_type":"user","peer_id":32,"version":4}`)
if len(dialogs.owners) != 1 || dialogs.owners[0] != 22 || dialogs.keys[0] != (domain.Peer{Type: domain.PeerTypeUser, ID: 32}) {

View file

@ -3,6 +3,7 @@ package postgres
import (
"context"
"encoding/json"
"time"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
@ -13,6 +14,8 @@ import (
const readModelChangeNotifyChannel = "telesrv_read_model_changed"
const privacyReadModelWarmTimeout = 5 * time.Second
// ReadModelCacheSet 是 read_model_versions 通知可失效的进程内投影缓存集合。
// 后续新增 read model 时,把缓存接到这里即可复用同一条 LISTEN 连接。
type ReadModelCacheSet struct {
@ -90,6 +93,14 @@ type PrivacyReadModelCache interface {
FlushReadModelCache()
}
// PrivacyReadModelWarmer lets the low-frequency change stream rebuild owner
// snapshots after invalidation, so the next user projection does not own a
// synchronous database miss. It is optional; caches without it remain
// cache-aside and only receive invalidation.
type PrivacyReadModelWarmer interface {
WarmOwners(context.Context, ...int64) error
}
type ProfilePhotoReadModelCache interface {
InvalidateOwner(domain.PeerType, int64)
FlushReadModelCache()
@ -393,6 +404,15 @@ func (l *ReadModelChangeListener) handlePayload(payload string) {
l.caches.RPCProjections.InvalidateRPCProjectionReadModelForUser(evt.OwnerUserID)
l.caches.RPCProjections.InvalidateRPCProjectionReadModelForViewer(evt.OwnerUserID)
}
if warmer, ok := l.caches.Privacy.(PrivacyReadModelWarmer); ok && evt.OwnerUserID != 0 {
ctx, cancel := context.WithTimeout(context.Background(), privacyReadModelWarmTimeout)
err := warmer.WarmOwners(ctx, evt.OwnerUserID)
cancel()
if err != nil {
l.log.Warn("warm privacy read model after change",
zap.Int64("owner_user_id", evt.OwnerUserID), zap.Error(err))
}
}
case "dialog_light":
if peerType, ok := readModelPeerType(evt.PeerType); ok && evt.OwnerUserID != 0 && evt.PeerID != 0 {
peer := domain.Peer{Type: peerType, ID: evt.PeerID}

View file

@ -1126,7 +1126,7 @@ WHERE sv.owner_peer_type = $1
OR lower(COALESCE(c.contact_last_name, u.last_name)) LIKE $7 ESCAPE '\'
OR lower(trim(COALESCE(NULLIF(c.contact_first_name, ''), u.first_name) || ' ' || COALESCE(c.contact_last_name, u.last_name))) LIKE $7 ESCAPE '\'
OR lower(u.username) LIKE $7 ESCAPE '\'
OR lower(COALESCE(NULLIF(c.contact_phone, ''), u.phone)) LIKE $7 ESCAPE '\'
OR lower(c.contact_phone) LIKE $7 ESCAPE '\'
)`, string(req.Owner.Type), req.Owner.ID, int32(req.StoryID), req.ViewerUserID, req.JustContacts, querySet, queryLike).Scan(&count); err != nil {
return domain.StoryViewList{}, fmt.Errorf("count story views: %w", err)
}
@ -1160,7 +1160,7 @@ WHERE sv.owner_peer_type = $1
OR lower(COALESCE(c.contact_last_name, u.last_name)) LIKE $7 ESCAPE '\'
OR lower(trim(COALESCE(NULLIF(c.contact_first_name, ''), u.first_name) || ' ' || COALESCE(c.contact_last_name, u.last_name))) LIKE $7 ESCAPE '\'
OR lower(u.username) LIKE $7 ESCAPE '\'
OR lower(COALESCE(NULLIF(c.contact_phone, ''), u.phone)) LIKE $7 ESCAPE '\'
OR lower(c.contact_phone) LIKE $7 ESCAPE '\'
)
AND (
NOT $9::boolean