diff --git a/internal/rpc/account.go b/internal/rpc/account.go index b4ab5b24..a1f6f00d 100644 --- a/internal/rpc/account.go +++ b/internal/rpc/account.go @@ -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)) diff --git a/internal/rpc/account_profile_emoji_test.go b/internal/rpc/account_profile_emoji_test.go index 46b2eb28..ee6d87e5 100644 --- a/internal/rpc/account_profile_emoji_test.go +++ b/internal/rpc/account_profile_emoji_test.go @@ -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: { diff --git a/internal/rpc/photos.go b/internal/rpc/photos.go index 5c0f16d9..0de92d93 100644 --- a/internal/rpc/photos.go +++ b/internal/rpc/photos.go @@ -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 { diff --git a/internal/rpc/photos_push_test.go b/internal/rpc/photos_push_test.go index 291399b1..53eee401 100644 --- a/internal/rpc/photos_push_test.go +++ b/internal/rpc/photos_push_test.go @@ -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() diff --git a/internal/rpc/router.go b/internal/rpc/router.go index d2e6d918..f5f03d12 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -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 } diff --git a/internal/rpc/send_media_test.go b/internal/rpc/send_media_test.go index 74a3250e..d2dee278 100644 --- a/internal/rpc/send_media_test.go +++ b/internal/rpc/send_media_test.go @@ -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) {