fix: sync avatar update compatibility fixes
This commit is contained in:
parent
2ad9dd8bb2
commit
44c32521e7
6 changed files with 374 additions and 17 deletions
|
|
@ -1583,6 +1583,10 @@ func (r *Router) onAccountGetDefaultBackgroundEmojis(ctx context.Context, hash i
|
|||
}
|
||||
|
||||
func defaultBackgroundEmojiDocumentIDs(ctx context.Context, files FilesService) ([]int64, error) {
|
||||
return statusPackEmojiDocumentIDs(ctx, files, 0)
|
||||
}
|
||||
|
||||
func statusPackEmojiDocumentIDs(ctx context.Context, files FilesService, limit int) ([]int64, error) {
|
||||
set, _, found, err := files.ResolveStickerSet(ctx, domain.StickerSetRef{
|
||||
Kind: domain.StickerSetRefByShortName,
|
||||
ShortName: "StatusPack",
|
||||
|
|
@ -1590,17 +1594,39 @@ func defaultBackgroundEmojiDocumentIDs(ctx context.Context, files FilesService)
|
|||
if err != nil || !found {
|
||||
return nil, err
|
||||
}
|
||||
return uniquePositiveDocumentIDs(set.DocumentIDs, 0), nil
|
||||
return uniquePositiveDocumentIDs(set.DocumentIDs, limit), nil
|
||||
}
|
||||
|
||||
func (r *Router) defaultProfilePhotoEmojiDocumentIDs(ctx context.Context, limit int) ([]int64, error) {
|
||||
ids, err := defaultProfilePhotoEmojiDocumentIDsFromKind(ctx, r.deps.Files, domain.StickerSetKindEmoji, limit, nil)
|
||||
ids, err := profilePhotoEmojiDocumentIDsFromRef(ctx, r.deps.Files, domain.StickerSetRef{
|
||||
Kind: domain.StickerSetRefByShortName,
|
||||
ShortName: "TelesrvDefaultStatuses",
|
||||
}, limit)
|
||||
if err != nil || len(ids) > 0 {
|
||||
return ids, err
|
||||
}
|
||||
return defaultProfilePhotoEmojiDocumentIDsFromKind(ctx, r.deps.Files, domain.StickerSetKindSystem, limit, func(set domain.StickerSet) bool {
|
||||
ids, err = defaultProfilePhotoEmojiDocumentIDsFromKind(ctx, r.deps.Files, domain.StickerSetKindSystem, limit, func(set domain.StickerSet) bool {
|
||||
return set.SystemKey == "animated_emoji"
|
||||
})
|
||||
if err != nil || len(ids) > 0 {
|
||||
return ids, err
|
||||
}
|
||||
ids, err = defaultProfilePhotoEmojiDocumentIDsFromKind(ctx, r.deps.Files, domain.StickerSetKindEmoji, limit, nil)
|
||||
if err != nil || len(ids) > 0 {
|
||||
return ids, err
|
||||
}
|
||||
return profilePhotoEmojiDocumentIDsFromRef(ctx, r.deps.Files, domain.StickerSetRef{
|
||||
Kind: domain.StickerSetRefByShortName,
|
||||
ShortName: "StatusPack",
|
||||
}, limit)
|
||||
}
|
||||
|
||||
func profilePhotoEmojiDocumentIDsFromRef(ctx context.Context, files FilesService, ref domain.StickerSetRef, limit int) ([]int64, error) {
|
||||
set, docs, found, err := files.ResolveStickerSet(ctx, ref)
|
||||
if err != nil || !found {
|
||||
return nil, err
|
||||
}
|
||||
return profilePhotoEmojiDocumentIDs(set.DocumentIDs, docs, limit), nil
|
||||
}
|
||||
|
||||
func defaultProfilePhotoEmojiDocumentIDsFromKind(
|
||||
|
|
@ -1620,7 +1646,12 @@ func defaultProfilePhotoEmojiDocumentIDsFromKind(
|
|||
if allow != nil && !allow(set) {
|
||||
continue
|
||||
}
|
||||
for _, id := range set.DocumentIDs {
|
||||
candidateIDs := uniquePositiveDocumentIDs(set.DocumentIDs, limit)
|
||||
docs, err := files.GetDocuments(ctx, candidateIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range profilePhotoEmojiDocumentIDs(candidateIDs, docs, limit) {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1640,6 +1671,40 @@ func defaultProfilePhotoEmojiDocumentIDsFromKind(
|
|||
return ids, nil
|
||||
}
|
||||
|
||||
func profilePhotoEmojiDocumentIDs(setIDs []int64, docs []domain.Document, limit int) []int64 {
|
||||
textColorDocs := make(map[int64]bool, len(docs))
|
||||
for _, doc := range docs {
|
||||
if documentHasTextColorCustomEmoji(doc) {
|
||||
textColorDocs[doc.ID] = true
|
||||
}
|
||||
}
|
||||
out := make([]int64, 0, len(setIDs))
|
||||
seen := make(map[int64]struct{}, len(setIDs))
|
||||
for _, id := range setIDs {
|
||||
if id <= 0 || textColorDocs[id] {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
if limit > 0 && len(out) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func documentHasTextColorCustomEmoji(doc domain.Document) bool {
|
||||
for _, attr := range doc.Attributes {
|
||||
if attr.Kind == domain.DocAttrCustomEmoji && attr.TextColor {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uniquePositiveDocumentIDs(ids []int64, limit int) []int64 {
|
||||
out := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
|
|
|
|||
|
|
@ -11,15 +11,12 @@ import (
|
|||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestAccountGetDefaultProfilePhotoEmojisUsesSeededEmojiSets(t *testing.T) {
|
||||
func TestAccountGetDefaultProfilePhotoEmojisUsesSeededEmojiSetsWhenSystemMissing(t *testing.T) {
|
||||
files := &fakeFiles{sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindEmoji: {
|
||||
{DocumentIDs: []int64{1001, 0, 1002, 1001}},
|
||||
{DocumentIDs: []int64{1003}},
|
||||
},
|
||||
domain.StickerSetKindSystem: {
|
||||
{SystemKey: "animated_emoji", DocumentIDs: []int64{2001}},
|
||||
},
|
||||
}}
|
||||
r := New(Config{}, Deps{Files: files}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
|
|
@ -47,6 +44,97 @@ func TestAccountGetDefaultProfilePhotoEmojisUsesSeededEmojiSets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAccountGetDefaultProfilePhotoEmojisPrefersSynthesizedDefaultStatuses(t *testing.T) {
|
||||
files := &fakeFiles{
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindEmoji: {
|
||||
{ShortName: "FestiveFontEmoji", DocumentIDs: []int64{9001, 9002}},
|
||||
},
|
||||
domain.StickerSetKindSystem: {
|
||||
{ShortName: "StatusPack", SystemKey: domain.StickerSetSystemKeyEmojiDefaultStatuses, DocumentIDs: []int64{1001, 0, 1002, 1001}},
|
||||
{ShortName: "TelesrvDefaultStatuses", SystemKey: domain.StickerSetSystemKeyEmojiDefaultStatuses, DocumentIDs: []int64{7001, 0, 7002, 7001}},
|
||||
{SystemKey: "animated_emoji", DocumentIDs: []int64{4001, 4002}},
|
||||
},
|
||||
},
|
||||
docs: map[int64]domain.Document{
|
||||
1001: {ID: 1001, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrCustomEmoji, TextColor: true}}},
|
||||
1002: {ID: 1002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrCustomEmoji, TextColor: true}}},
|
||||
7001: {ID: 7001, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
7002: {ID: 7002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
},
|
||||
}
|
||||
r := New(Config{}, Deps{Files: files}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
got, err := r.onAccountGetDefaultProfilePhotoEmojis(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get default profile photo emojis: %v", err)
|
||||
}
|
||||
list, ok := got.(*tg.EmojiList)
|
||||
if !ok {
|
||||
t.Fatalf("default profile photo emojis = %T, want *tg.EmojiList", got)
|
||||
}
|
||||
if len(list.DocumentID) != 2 || list.DocumentID[0] != 7001 || list.DocumentID[1] != 7002 {
|
||||
t.Fatalf("document ids = %v, want deduped synthesized default status ids", list.DocumentID)
|
||||
}
|
||||
if list.Hash == 0 {
|
||||
t.Fatal("emoji list hash = 0, want stable non-zero hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetDefaultProfilePhotoEmojisSkipsTextColorStatusPack(t *testing.T) {
|
||||
files := &fakeFiles{
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindSystem: {
|
||||
{ShortName: "StatusPack", SystemKey: domain.StickerSetSystemKeyEmojiDefaultStatuses, DocumentIDs: []int64{1001, 0, 1002, 1001}},
|
||||
},
|
||||
},
|
||||
docs: map[int64]domain.Document{
|
||||
1001: {ID: 1001, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrCustomEmoji, TextColor: true}}},
|
||||
1002: {ID: 1002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrCustomEmoji, TextColor: true}}},
|
||||
},
|
||||
}
|
||||
r := New(Config{}, Deps{Files: files}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
got, err := r.onAccountGetDefaultProfilePhotoEmojis(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get default profile photo emojis: %v", err)
|
||||
}
|
||||
list, ok := got.(*tg.EmojiList)
|
||||
if !ok {
|
||||
t.Fatalf("default profile photo emojis = %T, want *tg.EmojiList", got)
|
||||
}
|
||||
if len(list.DocumentID) != 0 {
|
||||
t.Fatalf("document ids = %v, want text-color StatusPack filtered out", list.DocumentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetDefaultProfilePhotoEmojisFallsBackToSystemBeforeCustomPacks(t *testing.T) {
|
||||
files := &fakeFiles{sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindEmoji: {
|
||||
{ShortName: "FestiveFontEmoji", DocumentIDs: []int64{9001, 9002}},
|
||||
},
|
||||
domain.StickerSetKindSystem: {
|
||||
{SystemKey: "animated_emoji", DocumentIDs: []int64{4001, 4002, 4001}},
|
||||
},
|
||||
}}
|
||||
r := New(Config{}, Deps{Files: files}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
got, err := r.onAccountGetDefaultProfilePhotoEmojis(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get default profile photo emojis: %v", err)
|
||||
}
|
||||
list, ok := got.(*tg.EmojiList)
|
||||
if !ok {
|
||||
t.Fatalf("default profile photo emojis = %T, want *tg.EmojiList", got)
|
||||
}
|
||||
if len(list.DocumentID) != 2 || list.DocumentID[0] != 4001 || list.DocumentID[1] != 4002 {
|
||||
t.Fatalf("document ids = %v, want animated_emoji ids before arbitrary custom packs", list.DocumentID)
|
||||
}
|
||||
if list.Hash == 0 {
|
||||
t.Fatal("emoji list hash = 0, want stable non-zero hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGetDefaultProfilePhotoEmojisFallsBackToSystemAnimatedEmoji(t *testing.T) {
|
||||
files := &fakeFiles{sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindSystem: {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/proto"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -438,10 +441,20 @@ func (r *Router) onPhotosDeletePhotos(ctx context.Context, id []tg.InputPhotoCla
|
|||
if len(ids) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
if _, err := r.deps.Files.DeleteProfilePhotos(ctx, domain.PeerTypeUser, userID, ids); err != nil {
|
||||
deleted, err := r.deps.Files.DeleteProfilePhotos(ctx, domain.PeerTypeUser, userID, ids)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
r.invalidateRPCProjectionForUser(userID)
|
||||
// 删除可能撤掉当前头像(回落到下一张或无头像):与 uploadProfilePhoto 同一纪律,
|
||||
// 向全部在线 session(含当前)推 updateUser + fresh self(对齐参考实现 deletePhotos 的
|
||||
// SyncPushUpdates),否则其它设备与当前设备(DrKLO 本地删除逻辑同样不重建 has_video)
|
||||
// 头像停留在旧状态。
|
||||
if deleted > 0 && r.deps.Users != nil {
|
||||
if self, err := r.deps.Users.Self(ctx, userID); err == nil {
|
||||
r.pushSelfPhotoUpdate(ctx, self)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
|
|
@ -517,16 +530,66 @@ func applyProfilePhotoToUser(user *domain.User, photo domain.Photo) {
|
|||
user.PhotoHasVideo = domain.PhotoHasVideo(photo.Sizes)
|
||||
}
|
||||
|
||||
// pushSelfPhotoUpdate 向该账号其它在线设备推送头像变更。updateUserName 不含 photo 无法刷新
|
||||
// 头像;updateUser 只是「该 user 变了」的信号(TDesktop 仅当 peer 已 full-loaded 时才
|
||||
// forceFull 重拉);最可靠是在 Updates.Users 带上含新 userProfilePhoto 的完整 self user,
|
||||
// TDesktop 经 processUser→setPhoto→peerUpdated(Photo) 即时刷新。当前设备同时经 RPC 返回更新。
|
||||
// pushSelfPhotoUpdate 向该账号全部在线设备(含当前 session)推送头像变更,对齐参考实现
|
||||
// SyncPushUpdates 的全 session 语义。updateUserName 不含 photo 无法刷新头像;updateUser 只是
|
||||
// 「该 user 变了」的信号(TDesktop 仅当 peer 已 full-loaded 时才 forceFull 重拉);最可靠是在
|
||||
// Updates.Users 带上含新 userProfilePhoto 的完整 self user,TDesktop 经
|
||||
// processUser→setPhoto→peerUpdated(Photo) 即时刷新。
|
||||
//
|
||||
// 当前 session 不能只依赖 RPC 返回:DrKLO Android 的 uploadProfilePhoto 响应回调
|
||||
// (ProfileActivity/SettingsActivity.didUploadPhoto、PhotoUtilities)不消费
|
||||
// photos.photo.users,而是手工重建 userProfilePhoto——只填 photo_id/photo_small/photo_big,
|
||||
// 丢掉 has_video/stripped_thumb;emoji/sticker markup 头像的本地渲染
|
||||
// (ImageReceiver.setForUserOrChat → VectorAvatarThumbDrawable)要求
|
||||
// user.photo.has_video==true,缺推送时该设备要等到下一次拿到 fresh self user(通常是重启)
|
||||
// 才会显示 emoji 头像。当前 session 的回显延迟发送,保证到达时客户端已处理完自己的
|
||||
// RPC 响应回调(否则 Android 回调会把推送刚修好的 photo 再次覆盖回无 has_video 的形状)。
|
||||
func (r *Router) pushSelfPhotoUpdate(ctx context.Context, self domain.User) {
|
||||
if self.ID == 0 {
|
||||
return
|
||||
}
|
||||
updates := selfPhotoUpdates(self, int(r.clock.Now().Unix()), r.tgSelfUser(self))
|
||||
r.pushUserUpdates(ctx, self.ID, updates)
|
||||
r.pushSelfPhotoUpdateToCurrentSession(ctx, updates)
|
||||
}
|
||||
|
||||
// defaultSelfPhotoEchoPushDelay 是头像变更后向当前 session 回显 updateUser 的延迟:
|
||||
// 必须晚于 RPC 结果写出与客户端响应回调,否则 DrKLO 的手工 photo 重建会覆盖回显内容。
|
||||
// updateUser 无 pts,晚到/丢失不影响 difference 正确性。
|
||||
const defaultSelfPhotoEchoPushDelay = 500 * time.Millisecond
|
||||
|
||||
// pushSelfPhotoUpdateToCurrentSession 延迟向当前 session 回显头像变更 updates。
|
||||
// ctx 值在调度前捕获(请求 ctx 在 handler 返回后即失效)。
|
||||
func (r *Router) pushSelfPhotoUpdateToCurrentSession(ctx context.Context, updates *tg.Updates) {
|
||||
if r.deps.Sessions == nil || updates == nil {
|
||||
return
|
||||
}
|
||||
sessionID, ok := SessionIDFrom(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rawAuthKeyID, hasRawAuthKeyID := RawAuthKeyIDFrom(ctx)
|
||||
push := func() {
|
||||
pushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if scoped, ok := r.scopedSessions(); ok {
|
||||
if !hasRawAuthKeyID {
|
||||
return
|
||||
}
|
||||
if err := scoped.PushToSessionForAuthKey(pushCtx, rawAuthKeyID, sessionID, proto.MessageFromServer, updates); err != nil {
|
||||
r.log.Debug("push self photo update to current session", zap.Int64("session_id", sessionID), zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := r.deps.Sessions.PushToSession(pushCtx, sessionID, proto.MessageFromServer, updates); err != nil {
|
||||
r.log.Debug("push self photo update to current session", zap.Int64("session_id", sessionID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
if r.selfPhotoEchoPushDelay <= 0 {
|
||||
push()
|
||||
return
|
||||
}
|
||||
time.AfterFunc(r.selfPhotoEchoPushDelay, push)
|
||||
}
|
||||
|
||||
func selfPhotoUpdates(self domain.User, date int, user tg.UserClass) *tg.Updates {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,132 @@ func TestUploadProfilePhotoPushesUpdateToOtherDevices(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestUploadProfilePhotoEchoesUpdateToCurrentSession 守护 DrKLO Android emoji 头像回显修复:
|
||||
// 换头像后除其它设备外,当前 session 也必须收到 updateUser + Updates.Users(带 has_video 的
|
||||
// fresh self)。DrKLO 的 uploadProfilePhoto 响应回调不消费 photos.photo.users,手工重建的
|
||||
// userProfilePhoto 丢 has_video/stripped,缺该回显时 emoji/sticker markup 头像在设置设备上
|
||||
// 要重启才渲染(对齐参考实现 SyncPushUpdates 全 session 语义)。
|
||||
func TestUploadProfilePhotoEchoesUpdateToCurrentSession(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550001004", FirstName: "Owner"})
|
||||
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,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
r.selfPhotoEchoPushDelay = 0 // 测试同步回显
|
||||
|
||||
const currentSessionID = int64(424242)
|
||||
reqCtx := WithSessionID(WithUserID(ctx, owner.ID), currentSessionID)
|
||||
req := &tg.PhotosUploadProfilePhotoRequest{}
|
||||
req.SetVideoEmojiMarkup(&tg.VideoSizeEmojiMarkup{EmojiID: 99, BackgroundColors: []int{0xffffff}})
|
||||
if _, err := r.onPhotosUploadProfilePhoto(reqCtx, req); err != nil {
|
||||
t.Fatalf("uploadProfilePhoto: %v", err)
|
||||
}
|
||||
|
||||
// PushToSession 在 PushToUserExceptSession 之后调用,snapshot().message 即当前 session 回显。
|
||||
snap := sessions.snapshot()
|
||||
if snap.sessionID != currentSessionID {
|
||||
t.Fatalf("echo session = %d, want %d", snap.sessionID, currentSessionID)
|
||||
}
|
||||
echoed, ok := snap.message.(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("echoed message = %T, want *tg.Updates", snap.message)
|
||||
}
|
||||
hasUserUpdate := false
|
||||
for _, u := range echoed.Updates {
|
||||
if uu, ok := u.(*tg.UpdateUser); ok && uu.UserID == owner.ID {
|
||||
hasUserUpdate = true
|
||||
}
|
||||
}
|
||||
if !hasUserUpdate {
|
||||
t.Fatalf("echoed updates = %+v, want UpdateUser for self", echoed.Updates)
|
||||
}
|
||||
if len(echoed.Users) == 0 {
|
||||
t.Fatal("echoed updates missing self user — current device cannot repair has_video")
|
||||
}
|
||||
echoedUser, ok := echoed.Users[0].(*tg.User)
|
||||
if !ok {
|
||||
t.Fatalf("echoed user = %T, want *tg.User", echoed.Users[0])
|
||||
}
|
||||
echoedPhoto, ok := echoedUser.Photo.(*tg.UserProfilePhoto)
|
||||
if !ok || echoedPhoto.PhotoID != 780 || !echoedPhoto.HasVideo {
|
||||
t.Fatalf("echoed self photo = %+v, want photo 780 has_video=true", echoedUser.Photo)
|
||||
}
|
||||
// 其它设备推送同样发生(同一 updates)。
|
||||
if other, ok := sessions.lastUserPush().(*tg.Updates); !ok || len(other.Users) == 0 {
|
||||
t.Fatalf("other-device push = %T, want *tg.Updates with users", sessions.lastUserPush())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeletePhotosPushesSelfUpdate 守护 deletePhotos 推送:删除生效(含撤掉当前头像回落)后
|
||||
// 必须向全部在线 session 推 updateUser + fresh self(对齐参考实现 deletePhotos 的
|
||||
// SyncPushUpdates),否则其它设备/当前设备头像停留旧状态。
|
||||
func TestDeletePhotosPushesSelfUpdate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550001005", FirstName: "Owner"})
|
||||
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,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
r.selfPhotoEchoPushDelay = 0
|
||||
|
||||
const currentSessionID = int64(434343)
|
||||
reqCtx := WithSessionID(WithUserID(ctx, owner.ID), currentSessionID)
|
||||
seedReq := &tg.PhotosUploadProfilePhotoRequest{}
|
||||
seedReq.SetFile(&tg.InputFile{ID: 42, Parts: 1, Name: "a.jpg"})
|
||||
if _, err := r.onPhotosUploadProfilePhoto(reqCtx, seedReq); err != nil {
|
||||
t.Fatalf("seed profile photo: %v", err)
|
||||
}
|
||||
sessions.clearMessages()
|
||||
|
||||
got, err := r.onPhotosDeletePhotos(reqCtx, []tg.InputPhotoClass{&tg.InputPhoto{ID: 778}})
|
||||
if err != nil {
|
||||
t.Fatalf("deletePhotos: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0] != 778 {
|
||||
t.Fatalf("deletePhotos result = %v, want [778]", got)
|
||||
}
|
||||
if _, found, err := files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, owner.ID, domain.ProfilePhotoKindProfile); err != nil || found {
|
||||
t.Fatalf("current profile photo after delete: found=%v err=%v, want removed", found, err)
|
||||
}
|
||||
pushed, ok := sessions.lastUserPush().(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("other-device push after delete = %T, want *tg.Updates", sessions.lastUserPush())
|
||||
}
|
||||
hasUserUpdate := false
|
||||
for _, u := range pushed.Updates {
|
||||
if uu, ok := u.(*tg.UpdateUser); ok && uu.UserID == owner.ID {
|
||||
hasUserUpdate = true
|
||||
}
|
||||
}
|
||||
if !hasUserUpdate || len(pushed.Users) == 0 {
|
||||
t.Fatalf("pushed updates after delete = %+v, want UpdateUser + self user", pushed)
|
||||
}
|
||||
pushedUser, ok := pushed.Users[0].(*tg.User)
|
||||
if !ok {
|
||||
t.Fatalf("pushed user = %T, want *tg.User", pushed.Users[0])
|
||||
}
|
||||
if _, stillHasPhoto := pushedUser.Photo.(*tg.UserProfilePhoto); stillHasPhoto {
|
||||
t.Fatalf("pushed self photo after delete = %+v, want cleared", pushedUser.Photo)
|
||||
}
|
||||
// 当前 session 也收到回显(PushToSession 最后调用,覆盖 snapshot().message)。
|
||||
snap := sessions.snapshot()
|
||||
if snap.sessionID != currentSessionID {
|
||||
t.Fatalf("echo session = %d, want %d", snap.sessionID, currentSessionID)
|
||||
}
|
||||
if _, ok := snap.message.(*tg.Updates); !ok {
|
||||
t.Fatalf("echoed message = %T, want *tg.Updates", snap.message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadProfilePhotoSupportsAnimatedVideoAndEmojiMarkup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ type Router struct {
|
|||
// 解析为卡片并就地替换。满则丢弃任务(消息留 pending)。nil=未启用(测试可直接调
|
||||
// resolvePendingWebPage 同步验证)。
|
||||
webPageResolveSem chan struct{}
|
||||
// selfPhotoEchoPushDelay 是头像变更后向当前 session 回显 updateUser 的延迟
|
||||
// (见 photos.go pushSelfPhotoUpdateToCurrentSession);<=0 时同步推送(测试用)。
|
||||
selfPhotoEchoPushDelay time.Duration
|
||||
}
|
||||
|
||||
type clientInfoSessionKey struct {
|
||||
|
|
@ -166,6 +169,7 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
|||
r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, presence: newPresenceTracker(), callbacks: newCallbackRegistry(), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), instanceID: instanceID}
|
||||
r.channelFanout = newChannelFanoutDispatcher(r, defaultChannelFanoutShards, defaultChannelFanoutBuffer)
|
||||
r.webPageResolveSem = make(chan struct{}, webPageResolveConcurrency)
|
||||
r.selfPhotoEchoPushDelay = defaultSelfPhotoEchoPushDelay
|
||||
if cfg.DC > 0 {
|
||||
groupCallStreamDCID = cfg.DC
|
||||
}
|
||||
|
|
|
|||
|
|
@ -694,11 +694,22 @@ func (f *fakeFiles) GetProfilePhotosKind(_ context.Context, _ domain.PeerType, _
|
|||
f.lastProfileMaxID = maxID
|
||||
return append([]domain.Photo(nil), f.profilePhotos...), f.profilePhotosTotal, nil
|
||||
}
|
||||
func (f *fakeFiles) DeleteProfilePhotos(context.Context, domain.PeerType, int64, []int64) (int, error) {
|
||||
return 0, nil
|
||||
func (f *fakeFiles) DeleteProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) (int, error) {
|
||||
return f.DeleteProfilePhotosKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photoIDs)
|
||||
}
|
||||
func (f *fakeFiles) DeleteProfilePhotosKind(context.Context, domain.PeerType, int64, domain.ProfilePhotoKind, []int64) (int, error) {
|
||||
return 0, nil
|
||||
func (f *fakeFiles) DeleteProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoIDs []int64) (int, error) {
|
||||
deleted := 0
|
||||
key := fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind}
|
||||
for _, id := range photoIDs {
|
||||
if _, ok := f.photos[id]; !ok {
|
||||
continue
|
||||
}
|
||||
deleted++
|
||||
if f.profile[key] == id {
|
||||
delete(f.profile, key)
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func newMediaTestRouter(t *testing.T) (*Router, domain.User, domain.User) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue