fix: sync avatar update compatibility fixes

This commit is contained in:
A 2026-07-06 16:16:19 +08:00
parent 2ad9dd8bb2
commit 44c32521e7
6 changed files with 374 additions and 17 deletions

View file

@ -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))

View file

@ -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: {

View file

@ -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 userTDesktop 经
// 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_thumbemoji/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 {

View file

@ -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()

View file

@ -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
}

View file

@ -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) {