fix for avatar updates
This commit is contained in:
parent
9cf53449fd
commit
a00f6ad814
2 changed files with 110 additions and 0 deletions
|
|
@ -642,6 +642,7 @@ func (r *Router) pushSelfPhotoUpdateWithUser(ctx context.Context, self domain.Us
|
||||||
updates := selfPhotoUpdates(self, int(r.clock.Now().Unix()), projected)
|
updates := selfPhotoUpdates(self, int(r.clock.Now().Unix()), projected)
|
||||||
r.pushUserUpdates(ctx, self.ID, updates)
|
r.pushUserUpdates(ctx, self.ID, updates)
|
||||||
r.pushSelfPhotoUpdateToCurrentSession(ctx, updates)
|
r.pushSelfPhotoUpdateToCurrentSession(ctx, updates)
|
||||||
|
r.pushPeerPhotoUpdate(ctx, self.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) pushBotPhotoUpdateToOwner(ctx context.Context, ownerUserID int64, bot domain.User, projected *tg.User) {
|
func (r *Router) pushBotPhotoUpdateToOwner(ctx context.Context, ownerUserID int64, bot domain.User, projected *tg.User) {
|
||||||
|
|
@ -651,6 +652,54 @@ func (r *Router) pushBotPhotoUpdateToOwner(ctx context.Context, ownerUserID int6
|
||||||
updates := selfPhotoUpdates(bot, int(r.clock.Now().Unix()), projected)
|
updates := selfPhotoUpdates(bot, int(r.clock.Now().Unix()), projected)
|
||||||
r.pushUserUpdates(ctx, ownerUserID, updates)
|
r.pushUserUpdates(ctx, ownerUserID, updates)
|
||||||
r.pushSelfPhotoUpdateToCurrentSession(ctx, updates)
|
r.pushSelfPhotoUpdateToCurrentSession(ctx, updates)
|
||||||
|
// The bot's own dialog peers (anyone who has chatted with it) never
|
||||||
|
// received anything here before -- only the human managing the bot's
|
||||||
|
// profile did (above). A bot has no contacts, so presenceFanoutCandidates
|
||||||
|
// resolves to its private-dialog peers only, which is exactly who needs
|
||||||
|
// to see a changed bot avatar.
|
||||||
|
r.pushPeerPhotoUpdate(ctx, bot.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushPeerPhotoUpdate notifies ownerUserID's contacts and private-dialog
|
||||||
|
// peers that its profile photo changed, reusing presenceFanoutCandidates
|
||||||
|
// (the same "contacts ∪ private-dialog peers" set presence pushes already
|
||||||
|
// compute) -- until this existed, a changed avatar was never pushed to
|
||||||
|
// anyone except the owning account's own other sessions, so a contact's chat
|
||||||
|
// window kept showing the old avatar until they happened to reopen the chat
|
||||||
|
// or restart their client.
|
||||||
|
//
|
||||||
|
// Sends only a bare tg.UpdateUser{UserID: ownerUserID} signal, never an
|
||||||
|
// embedded tg.User: unlike the owner's own devices (which can always see
|
||||||
|
// their own photo, so pushSelfPhotoUpdateWithUser safely embeds it), a
|
||||||
|
// peer's view of ownerUserID's photo is privacy-gated
|
||||||
|
// (domain.PrivacyKeyProfilePhoto) -- baking the raw photo in here would
|
||||||
|
// bypass that gate. The bare signal instead makes the recipient's client
|
||||||
|
// re-fetch ownerUserID through the ordinary users/getFullUser path, which
|
||||||
|
// already applies that privacy check correctly.
|
||||||
|
//
|
||||||
|
// Uses the durable per-recipient push path (pushUserMessage), not the
|
||||||
|
// transient one presence uses: a missed avatar refresh is far stickier and
|
||||||
|
// more noticeable than a missed status blip, so a recipient who is offline
|
||||||
|
// right now should still pick this up on their next reconnect via normal
|
||||||
|
// update dispatch, not just while they happen to be online already.
|
||||||
|
func (r *Router) pushPeerPhotoUpdate(ctx context.Context, ownerUserID int64) {
|
||||||
|
if ownerUserID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recipients := r.presenceFanoutCandidates(ctx, ownerUserID)
|
||||||
|
if len(recipients) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
update := &tg.Updates{
|
||||||
|
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: ownerUserID}},
|
||||||
|
Date: int(r.clock.Now().Unix()),
|
||||||
|
}
|
||||||
|
for _, recipientID := range recipients {
|
||||||
|
if recipientID == 0 || recipientID == ownerUserID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.pushUserMessage(ctx, recipientID, "push peer photo update", update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultSelfPhotoEchoPushDelay 是头像变更后向当前 session 回显 updateUser 的延迟:
|
// defaultSelfPhotoEchoPushDelay 是头像变更后向当前 session 回显 updateUser 的延迟:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
botsapp "telesrv/internal/app/bots"
|
botsapp "telesrv/internal/app/bots"
|
||||||
|
dialogsapp "telesrv/internal/app/dialogs"
|
||||||
appusers "telesrv/internal/app/users"
|
appusers "telesrv/internal/app/users"
|
||||||
"telesrv/internal/domain"
|
"telesrv/internal/domain"
|
||||||
"telesrv/internal/store/memory"
|
"telesrv/internal/store/memory"
|
||||||
|
|
@ -483,3 +484,63 @@ func TestUploadProfilePhotoFallbackEmojiAndInvalidFlags(t *testing.T) {
|
||||||
t.Fatalf("current profile photo after invalid = %+v ok=%v err=%v, want preserved 778", cur, ok, err)
|
t.Fatalf("current profile photo after invalid = %+v ok=%v err=%v, want preserved 778", cur, ok, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUploadProfilePhotoNotifiesPrivateDialogPeer guards the fix for "avatars
|
||||||
|
// never refresh live": before pushPeerPhotoUpdate existed, a changed avatar
|
||||||
|
// was pushed only to the owner's own other sessions (see
|
||||||
|
// TestUploadProfilePhotoPushesUpdateToOtherDevices above) -- a private-dialog
|
||||||
|
// peer got nothing at all and kept showing the stale avatar until they
|
||||||
|
// happened to reopen the chat or restart their client. peer must now receive
|
||||||
|
// a bare UpdateUser{UserID: owner} signal (never an embedded tg.User -- see
|
||||||
|
// pushPeerPhotoUpdate's doc comment on why the raw photo must not be baked in
|
||||||
|
// for a privacy-gated peer view).
|
||||||
|
func TestUploadProfilePhotoNotifiesPrivateDialogPeer(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userStore := memory.NewUserStore()
|
||||||
|
dialogStore := memory.NewDialogStore()
|
||||||
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550001009", FirstName: "Owner"})
|
||||||
|
peer, _ := userStore.Create(ctx, domain.User{AccessHash: 12, Phone: "15550001010", FirstName: "Peer"})
|
||||||
|
// A private dialog from owner's side is exactly what marks peer as a
|
||||||
|
// presence/photo fan-out candidate -- see presenceFanoutCandidates.
|
||||||
|
if err := dialogStore.Upsert(ctx, owner.ID, domain.Dialog{Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peer.ID}}); err != nil {
|
||||||
|
t.Fatalf("seed dialog: %v", err)
|
||||||
|
}
|
||||||
|
sessions := &captureSessions{}
|
||||||
|
files := &fakeFiles{}
|
||||||
|
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||||
|
Users: appusers.NewService(userStore, appusers.WithPhotoProvider(files)),
|
||||||
|
Files: files,
|
||||||
|
Sessions: sessions,
|
||||||
|
Dialogs: dialogsapp.NewService(dialogStore),
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
req := &tg.PhotosUploadProfilePhotoRequest{}
|
||||||
|
req.SetFile(&tg.InputFile{ID: 42, Parts: 1, Name: "a.jpg"})
|
||||||
|
if _, err := r.onPhotosUploadProfilePhoto(WithUserID(ctx, owner.ID), req); err != nil {
|
||||||
|
t.Fatalf("uploadProfilePhoto: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushPeerPhotoUpdate runs last inside pushSelfPhotoUpdateWithUser (after
|
||||||
|
// the owner's own-session push), so it's both the last pushed user id and
|
||||||
|
// the last recorded userMessage -- see that function's call order.
|
||||||
|
pushed := sessions.pushedUserIDs()
|
||||||
|
if len(pushed) == 0 || pushed[len(pushed)-1] != peer.ID {
|
||||||
|
t.Fatalf("pushed user ids = %v, want peer %d last", pushed, peer.ID)
|
||||||
|
}
|
||||||
|
peerUpdates, ok := sessions.lastUserPush().(*tg.Updates)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("peer push = %T, want *tg.Updates", sessions.lastUserPush())
|
||||||
|
}
|
||||||
|
hasSignal := false
|
||||||
|
for _, u := range peerUpdates.Updates {
|
||||||
|
if uu, ok := u.(*tg.UpdateUser); ok && uu.UserID == owner.ID {
|
||||||
|
hasSignal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasSignal {
|
||||||
|
t.Fatalf("peer updates = %+v, want bare UpdateUser signal for owner", peerUpdates.Updates)
|
||||||
|
}
|
||||||
|
if len(peerUpdates.Users) != 0 {
|
||||||
|
t.Fatalf("peer updates.Users = %+v, want empty -- raw photo must not bypass privacy for a peer push", peerUpdates.Users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue