fix: sync Android channel compatibility fixes
This commit is contained in:
parent
b1f74185f0
commit
7fa721a25e
16 changed files with 566 additions and 32 deletions
|
|
@ -31,6 +31,9 @@ const tdesktopClient = "tdesktop"
|
|||
// true(屏蔽),GiftSheet:967 据此隐藏整个 star gift 送礼网格——缺 key 则送礼选择器恒空。
|
||||
// - reactions_user_max_premium=3 与服务端 domain.MaxMessageReactionsPerUserPremium
|
||||
// 联动:premium 用户可在同一消息放 3 个 reaction,服务端档位必须 ≥ 该宣告值。
|
||||
// - boosts_channel_level_max=100 必须显式下发:DrKLO 的频道自定义 reaction 编辑页
|
||||
// 用它作为可选 reaction 个数的本地 LengthFilter 上限。缺 key 会保留旧偏好值;实测
|
||||
// 旧值为 4 时,频道已选 4 个 reaction 后继续点新 emoji 会被客户端本地静默挡掉。
|
||||
// - dialog_filters_enabled=true 必须显式下发:TDesktop settings_main.cpp:394 据此(或账号
|
||||
// 已有文件夹)才在 Settings 显示「Folders」入口,缺 key → 新账号看不到文件夹管理、无法
|
||||
// 建文件夹/采纳 getSuggestedDialogFilters 模板。
|
||||
|
|
@ -43,9 +46,9 @@ const tdesktopClient = "tdesktop"
|
|||
// 隐身模式本地 UI/乐观状态用的时间常量,与当前 bounded stealth update stub 保持一致。
|
||||
// - aicompose_tone_* 与 domain/app/ai 默认值一致:TDesktop/DrKLO 创建/预览 tone 时
|
||||
// 直接读取这些 key 做本地输入限制和示例数量。
|
||||
const tdesktopDefaultAppConfigBase = `{"chat_read_mark_expire_period":604800,"chat_read_mark_size_threshold":50,"pm_read_date_expire_period":604800,"quote_length_max":1024,"telegram_antispam_group_size_min":200,"telegram_antispam_user_id":"5434988373","forum_upgrade_participants_min":2,"reactions_default":{"_":"reactionEmoji","emoticon":"👍"},"reactions_uniq_max":11,"reactions_user_max_default":1,"reactions_user_max_premium":3,"reactions_in_chat_max":3,"upload_markup_video":true,"emojies_send_dice":["🎲","🎯","🏀","⚽","⚽️","🎳","🎰"],"premium_purchase_blocked":false,"stargifts_blocked":false,"stories_stealth_future_period":1500,"stories_stealth_past_period":300,"stories_stealth_cooldown_period":10800,"quick_replies_limit":100,"quick_reply_messages_limit":20,"business_chat_links_limit":100,"dialog_filters_enabled":true,"about_length_limit_default":70,"about_length_limit_premium":140,"caption_length_limit_default":1024,"caption_length_limit_premium":4096,"channels_limit_default":500,"channels_limit_premium":1000,"channels_public_limit_default":10,"channels_public_limit_premium":20,"dialog_filters_limit_default":10,"dialog_filters_limit_premium":20,"dialog_filters_chats_limit_default":100,"dialog_filters_chats_limit_premium":200,"dialogs_pinned_limit_default":5,"dialogs_pinned_limit_premium":10,"dialogs_folder_pinned_limit_default":100,"dialogs_folder_pinned_limit_premium":200,"saved_dialogs_pinned_limit_default":5,"saved_dialogs_pinned_limit_premium":100,"saved_gifs_limit_default":200,"saved_gifs_limit_premium":400,"stickers_faved_limit_default":5,"stickers_faved_limit_premium":10,"recommended_channels_limit_default":10,"recommended_channels_limit_premium":100,"aicompose_tone_examples_num":3,"aicompose_tone_title_length_max":12,"aicompose_tone_prompt_length_max":1024,"aicompose_tone_saved_limit_default":5,"aicompose_tone_saved_limit_premium":20,"upload_max_fileparts_default":4000,"upload_max_fileparts_premium":8000`
|
||||
const tdesktopDefaultAppConfigBase = `{"chat_read_mark_expire_period":604800,"chat_read_mark_size_threshold":50,"pm_read_date_expire_period":604800,"quote_length_max":1024,"telegram_antispam_group_size_min":200,"telegram_antispam_user_id":"5434988373","forum_upgrade_participants_min":2,"reactions_default":{"_":"reactionEmoji","emoticon":"👍"},"reactions_uniq_max":11,"reactions_user_max_default":1,"reactions_user_max_premium":3,"reactions_in_chat_max":3,"boosts_channel_level_max":100,"upload_markup_video":true,"emojies_send_dice":["🎲","🎯","🏀","⚽","⚽️","🎳","🎰"],"premium_purchase_blocked":false,"stargifts_blocked":false,"stories_stealth_future_period":1500,"stories_stealth_past_period":300,"stories_stealth_cooldown_period":10800,"quick_replies_limit":100,"quick_reply_messages_limit":20,"business_chat_links_limit":100,"dialog_filters_enabled":true,"about_length_limit_default":70,"about_length_limit_premium":140,"caption_length_limit_default":1024,"caption_length_limit_premium":4096,"channels_limit_default":500,"channels_limit_premium":1000,"channels_public_limit_default":10,"channels_public_limit_premium":20,"dialog_filters_limit_default":10,"dialog_filters_limit_premium":20,"dialog_filters_chats_limit_default":100,"dialog_filters_chats_limit_premium":200,"dialogs_pinned_limit_default":5,"dialogs_pinned_limit_premium":10,"dialogs_folder_pinned_limit_default":100,"dialogs_folder_pinned_limit_premium":200,"saved_dialogs_pinned_limit_default":5,"saved_dialogs_pinned_limit_premium":100,"saved_gifs_limit_default":200,"saved_gifs_limit_premium":400,"stickers_faved_limit_default":5,"stickers_faved_limit_premium":10,"recommended_channels_limit_default":10,"recommended_channels_limit_premium":100,"aicompose_tone_examples_num":3,"aicompose_tone_title_length_max":12,"aicompose_tone_prompt_length_max":1024,"aicompose_tone_saved_limit_default":5,"aicompose_tone_saved_limit_premium":20,"upload_max_fileparts_default":4000,"upload_max_fileparts_premium":8000`
|
||||
|
||||
const defaultAppConfigHash = 18 // 默认 app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
const defaultAppConfigHash = 19 // 默认 app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
|
||||
// Service 提供客户端启动配置与国家区号目录。
|
||||
//
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func TestAppConfigPremiumKeys(t *testing.T) {
|
|||
wantNumbers := map[string]float64{
|
||||
"reactions_user_max_default": 1,
|
||||
"reactions_user_max_premium": 3,
|
||||
"boosts_channel_level_max": 100,
|
||||
"about_length_limit_default": 70,
|
||||
"about_length_limit_premium": 140,
|
||||
"dialogs_pinned_limit_default": 5,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
appConfigHash = 13 // app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
appConfigHash = 14 // app config 内容变更时必须递增,否则缓存端只会收到 notModified。
|
||||
countriesListHash = 1
|
||||
timezonesListHash = 1
|
||||
)
|
||||
|
|
@ -38,6 +38,8 @@ func readMarkAppConfig(mapboxToken string) *tg.JSONObject {
|
|||
// stargifts_blocked=false:DrKLO 缺省 stargiftsBlocked=true 会隐藏 star gift 送礼网格。
|
||||
{Key: "stargifts_blocked", Value: &tg.JSONBool{Value: false}},
|
||||
{Key: "reactions_user_max_premium", Value: &tg.JSONNumber{Value: 3}},
|
||||
// DrKLO 频道自定义 reaction 编辑页用它作为可选 reaction 数量上限。
|
||||
{Key: "boosts_channel_level_max", Value: &tg.JSONNumber{Value: 100}},
|
||||
// dialog_filters_enabled=true:TDesktop 据此(或已有文件夹)才显示 Settings→Folders 入口。
|
||||
{Key: "dialog_filters_enabled", Value: &tg.JSONBool{Value: true}},
|
||||
{Key: "stories_stealth_future_period", Value: &tg.JSONNumber{Value: 1500}},
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputCha
|
|||
r.applyStarGiftsCountToChannelFull(ctx, ref.ID, &full)
|
||||
r.applyStoriesPinnedAvailableToChannelFull(ctx, userID, ref.ID, &full)
|
||||
r.applyNotifySettingsToChannelFull(ctx, userID, ref.ID, &full)
|
||||
r.applyAndroidChannelReactionEditorCompat(ctx, &full, cached.canChangeInfo)
|
||||
chats := append([]tg.ChatClass(nil), cached.chats...)
|
||||
r.trackChannelInterest(ctx, userID, ref.ID)
|
||||
r.applyStoryMaxIDsToPeerObjects(ctx, userID, nil, chats)
|
||||
|
|
@ -153,14 +154,17 @@ func (r *Router) onChannelsGetFullChannel(ctx context.Context, input tg.InputCha
|
|||
// 当前频道默认已由 tgChannelFull 处理;外部频道默认(以自己拥有的别的频道身份发言)需在此投影并
|
||||
// 带上该频道对象,否则客户端拿不到默认 chip。
|
||||
r.applyForeignDefaultSendAsToFull(ctx, userID, view, full, &chats)
|
||||
canChangeInfo := channelMemberCanChangeInfo(view.Self)
|
||||
r.channelFullProjectionCache.StoreIfEpoch(userID, view.Channel.ID, channelFullProjection{
|
||||
accessHash: view.Channel.AccessHash,
|
||||
full: *full,
|
||||
chats: append([]tg.ChatClass(nil), chats...),
|
||||
userIDs: userIDs,
|
||||
accessHash: view.Channel.AccessHash,
|
||||
canChangeInfo: canChangeInfo,
|
||||
full: *full,
|
||||
chats: append([]tg.ChatClass(nil), chats...),
|
||||
userIDs: userIDs,
|
||||
}, loadEpoch)
|
||||
r.applyStoriesPinnedAvailableToChannelFull(ctx, userID, view.Channel.ID, full)
|
||||
r.applyNotifySettingsToChannelFull(ctx, userID, view.Channel.ID, full)
|
||||
r.applyAndroidChannelReactionEditorCompat(ctx, full, canChangeInfo)
|
||||
r.applyStoryMaxIDsToPeerObjects(ctx, userID, nil, chats)
|
||||
return &tg.MessagesChatFull{
|
||||
FullChat: full,
|
||||
|
|
|
|||
|
|
@ -609,6 +609,9 @@ func (r *Router) onMessagesSetChatAvailableReactions(ctx context.Context, req *t
|
|||
if r.deps.Channels == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if req == nil {
|
||||
return nil, tgerr400("REACTION_INVALID")
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
|
|
@ -617,7 +620,12 @@ func (r *Router) onMessagesSetChatAvailableReactions(ctx context.Context, req *t
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy, err := domainChannelReactionPolicy(req)
|
||||
current, err := r.deps.Channels.GetChannelForChangeInfo(ctx, userID, channelID)
|
||||
if err != nil {
|
||||
return nil, channelAdminErr(err)
|
||||
}
|
||||
_, defaultReactionDocuments := r.availableReactionDocumentMaps(ctx)
|
||||
policy, err := domainChannelReactionPolicy(req, current.Channel.ReactionPolicy, defaultReactionDocuments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,11 +51,12 @@ func TestLegacyChannelSettingsRPC(t *testing.T) {
|
|||
t.Fatalf("private set chat theme updates = %+v, want empty compat ack", privateTheme)
|
||||
}
|
||||
|
||||
reactionUpdates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
setReactionsReq := &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "\U0001f44d"}}},
|
||||
ReactionsLimit: 8,
|
||||
})
|
||||
}
|
||||
setReactionsReq.SetReactionsLimit(8)
|
||||
reactionUpdates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), setReactionsReq)
|
||||
if err != nil {
|
||||
t.Fatalf("set available reactions: %v", err)
|
||||
}
|
||||
|
|
@ -148,11 +149,12 @@ func TestBroadcastChannelAcceptsFullReactionCatalog(t *testing.T) {
|
|||
for i := 0; i < catalogSize; i++ {
|
||||
reactions = append(reactions, &tg.ReactionEmoji{Emoticon: fmt.Sprintf("r%02d", i)})
|
||||
}
|
||||
updates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
setCatalogReq := &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: reactions},
|
||||
ReactionsLimit: 11,
|
||||
})
|
||||
}
|
||||
setCatalogReq.SetReactionsLimit(11)
|
||||
updates, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), setCatalogReq)
|
||||
if err != nil {
|
||||
t.Fatalf("set full-catalog reactions on broadcast channel: %v", err)
|
||||
}
|
||||
|
|
@ -173,4 +175,242 @@ func TestBroadcastChannelAcceptsFullReactionCatalog(t *testing.T) {
|
|||
if !ok || len(some.Reactions) != catalogSize {
|
||||
t.Fatalf("full channel reactions = %#v, want %d explicit reactions", stored, catalogSize)
|
||||
}
|
||||
if fullChannel.GetPaidReactionsAvailable() {
|
||||
t.Fatalf("full channel paid reactions = true, want false without paid_enabled flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChatAvailableReactionsPreservesOptionalFlags(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 101, Phone: "15550002201", FirstName: "Owner"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
|
||||
Title: "Broadcast Optional Reactions",
|
||||
Broadcast: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create broadcast channel: %v", err)
|
||||
}
|
||||
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
|
||||
initial := &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsAll{AllowCustom: true},
|
||||
}
|
||||
initial.SetReactionsLimit(7)
|
||||
initial.SetPaidEnabled(true)
|
||||
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), initial); err != nil {
|
||||
t.Fatalf("set initial reaction policy: %v", err)
|
||||
}
|
||||
|
||||
omitOptional := &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
|
||||
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
|
||||
}},
|
||||
}
|
||||
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), omitOptional); err != nil {
|
||||
t.Fatalf("set reaction policy without optional flags: %v", err)
|
||||
}
|
||||
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
||||
if err != nil {
|
||||
t.Fatalf("get full channel after omitted flags: %v", err)
|
||||
}
|
||||
fullChannel := full.FullChat.(*tg.ChannelFull)
|
||||
if fullChannel.ReactionsLimit != 7 {
|
||||
t.Fatalf("reactions limit after omitted flag = %d, want preserved 7", fullChannel.ReactionsLimit)
|
||||
}
|
||||
if !fullChannel.GetPaidReactionsAvailable() {
|
||||
t.Fatalf("paid reactions after omitted flag = false, want preserved true")
|
||||
}
|
||||
|
||||
disablePaid := &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
|
||||
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
|
||||
&tg.ReactionEmoji{Emoticon: "\u2764"},
|
||||
}},
|
||||
}
|
||||
disablePaid.SetPaidEnabled(false)
|
||||
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), disablePaid); err != nil {
|
||||
t.Fatalf("disable paid reactions without limit flag: %v", err)
|
||||
}
|
||||
full, err = r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
||||
if err != nil {
|
||||
t.Fatalf("get full channel after paid disable: %v", err)
|
||||
}
|
||||
fullChannel = full.FullChat.(*tg.ChannelFull)
|
||||
if fullChannel.ReactionsLimit != 7 {
|
||||
t.Fatalf("reactions limit after paid-only update = %d, want preserved 7", fullChannel.ReactionsLimit)
|
||||
}
|
||||
if fullChannel.GetPaidReactionsAvailable() {
|
||||
t.Fatalf("paid reactions after explicit false = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndroidChannelReactionEditorProjectsDefaultEmojiAsDocuments(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 111, Phone: "15550002211", FirstName: "Owner"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
files := &fakeFiles{reactions: []domain.AvailableReaction{
|
||||
{Reaction: "\U0001f44d", ActivateAnimationID: 7101},
|
||||
{Reaction: "\U0001f525", ActivateAnimationID: 7102},
|
||||
}}
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
Files: files,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
|
||||
Title: "Android Reaction Projection",
|
||||
Broadcast: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create broadcast channel: %v", err)
|
||||
}
|
||||
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
|
||||
&tg.ReactionEmoji{Emoticon: "\U0001f44d"},
|
||||
&tg.ReactionEmoji{Emoticon: "\U0001f525"},
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatalf("set reaction policy: %v", err)
|
||||
}
|
||||
|
||||
desktopFull, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
||||
if err != nil {
|
||||
t.Fatalf("get desktop full channel: %v", err)
|
||||
}
|
||||
desktopSome := mustChannelFullSomeReactions(t, desktopFull)
|
||||
if emoji, ok := desktopSome.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
|
||||
t.Fatalf("desktop reaction[0] = %T %+v, want reactionEmoji thumbs up", desktopSome.Reactions[0], desktopSome.Reactions[0])
|
||||
}
|
||||
|
||||
androidCtx := WithClientInfo(WithUserID(ctx, owner.ID), ClientInfo{Type: ClientTypeAndroid, AppVersion: "12.8.1"})
|
||||
androidFull, err := r.onChannelsGetFullChannel(androidCtx, &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
||||
if err != nil {
|
||||
t.Fatalf("get android full channel: %v", err)
|
||||
}
|
||||
androidSome := mustChannelFullSomeReactions(t, androidFull)
|
||||
if doc, ok := androidSome.Reactions[0].(*tg.ReactionCustomEmoji); !ok || doc.DocumentID != 7101 {
|
||||
t.Fatalf("android reaction[0] = %T %+v, want reactionCustomEmoji 7101", androidSome.Reactions[0], androidSome.Reactions[0])
|
||||
}
|
||||
if doc, ok := androidSome.Reactions[1].(*tg.ReactionCustomEmoji); !ok || doc.DocumentID != 7102 {
|
||||
t.Fatalf("android reaction[1] = %T %+v, want reactionCustomEmoji 7102", androidSome.Reactions[1], androidSome.Reactions[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChatAvailableReactionsNormalizesDefaultReactionDocuments(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 112, Phone: "15550002212", FirstName: "Owner"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
files := &fakeFiles{reactions: []domain.AvailableReaction{
|
||||
{Reaction: "\U0001f44d", ActivateAnimationID: 7201},
|
||||
}}
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
Files: files,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := r.onChannelsCreateChannel(WithUserID(ctx, owner.ID), &tg.ChannelsCreateChannelRequest{
|
||||
Title: "Android Reaction Save",
|
||||
Broadcast: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create broadcast channel: %v", err)
|
||||
}
|
||||
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
if _, err := r.onMessagesSetChatAvailableReactions(WithUserID(ctx, owner.ID), &tg.MessagesSetChatAvailableReactionsRequest{
|
||||
Peer: peer,
|
||||
AvailableReactions: &tg.ChatReactionsSome{Reactions: []tg.ReactionClass{
|
||||
&tg.ReactionCustomEmoji{DocumentID: 7201},
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatalf("set reaction policy with default document id: %v", err)
|
||||
}
|
||||
|
||||
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, owner.ID), &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
||||
if err != nil {
|
||||
t.Fatalf("get full channel: %v", err)
|
||||
}
|
||||
some := mustChannelFullSomeReactions(t, full)
|
||||
if emoji, ok := some.Reactions[0].(*tg.ReactionEmoji); !ok || emoji.Emoticon != "\U0001f44d" {
|
||||
t.Fatalf("stored reaction[0] = %T %+v, want normalized reactionEmoji thumbs up", some.Reactions[0], some.Reactions[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableReactionDocumentMapsAreCached(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &countingAvailableReactionFiles{fakeFiles: &fakeFiles{reactions: []domain.AvailableReaction{
|
||||
{Reaction: "\U0001f44d", ActivateAnimationID: 7301},
|
||||
}}}
|
||||
r := &Router{deps: Deps{Files: files}}
|
||||
|
||||
emojiToDoc, docToEmoji := r.availableReactionDocumentMaps(ctx)
|
||||
if got := emojiToDoc["\U0001f44d"]; got != 7301 {
|
||||
t.Fatalf("emoji->document map = %d, want 7301", got)
|
||||
}
|
||||
if got := docToEmoji[7301]; got != "\U0001f44d" {
|
||||
t.Fatalf("document->emoji map = %q, want thumbs up", got)
|
||||
}
|
||||
|
||||
files.fakeFiles.reactions = append(files.fakeFiles.reactions, domain.AvailableReaction{
|
||||
Reaction: "\U0001f525",
|
||||
ActivateAnimationID: 7302,
|
||||
})
|
||||
emojiToDoc, docToEmoji = r.availableReactionDocumentMaps(ctx)
|
||||
if files.calls != 1 {
|
||||
t.Fatalf("ListAvailableReactions calls = %d, want 1 cached load", files.calls)
|
||||
}
|
||||
if got := emojiToDoc["\U0001f525"]; got != 0 {
|
||||
t.Fatalf("cached emoji->document map unexpectedly saw later catalog mutation: %d", got)
|
||||
}
|
||||
if got := docToEmoji[7302]; got != "" {
|
||||
t.Fatalf("cached document->emoji map unexpectedly saw later catalog mutation: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type countingAvailableReactionFiles struct {
|
||||
*fakeFiles
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *countingAvailableReactionFiles) ListAvailableReactions(ctx context.Context) ([]domain.AvailableReaction, error) {
|
||||
f.calls++
|
||||
return f.fakeFiles.ListAvailableReactions(ctx)
|
||||
}
|
||||
|
||||
func mustChannelFullSomeReactions(t *testing.T, full *tg.MessagesChatFull) *tg.ChatReactionsSome {
|
||||
t.Helper()
|
||||
channelFull, ok := full.FullChat.(*tg.ChannelFull)
|
||||
if !ok {
|
||||
t.Fatalf("full chat = %T, want *tg.ChannelFull", full.FullChat)
|
||||
}
|
||||
reactions, ok := channelFull.GetAvailableReactions()
|
||||
if !ok {
|
||||
t.Fatalf("channel full reactions missing")
|
||||
}
|
||||
some, ok := reactions.(*tg.ChatReactionsSome)
|
||||
if !ok {
|
||||
t.Fatalf("channel full reactions = %T %+v, want *tg.ChatReactionsSome", reactions, reactions)
|
||||
}
|
||||
if len(some.Reactions) == 0 {
|
||||
t.Fatalf("channel full reactions empty")
|
||||
}
|
||||
return some
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,16 +83,22 @@ func (r *Router) onChannelsReadHistory(ctx context.Context, req *tg.ChannelsRead
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func domainChannelReactionPolicy(req *tg.MessagesSetChatAvailableReactionsRequest) (domain.ChannelReactionPolicy, error) {
|
||||
func domainChannelReactionPolicy(req *tg.MessagesSetChatAvailableReactionsRequest, current domain.ChannelReactionPolicy, defaultReactionDocuments map[int64]string) (domain.ChannelReactionPolicy, error) {
|
||||
if req == nil || req.AvailableReactions == nil {
|
||||
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
||||
}
|
||||
if req.ReactionsLimit < 0 || req.ReactionsLimit > domain.MaxChannelReactionsLimit {
|
||||
return domain.ChannelReactionPolicy{}, limitInvalidErr()
|
||||
}
|
||||
policy := domain.ChannelReactionPolicy{
|
||||
Limit: req.ReactionsLimit,
|
||||
PaidEnabled: req.PaidEnabled,
|
||||
Limit: current.Limit,
|
||||
PaidEnabled: current.PaidEnabled,
|
||||
}
|
||||
if limit, ok := req.GetReactionsLimit(); ok {
|
||||
if limit < 0 || limit > domain.MaxChannelReactionsLimit {
|
||||
return domain.ChannelReactionPolicy{}, limitInvalidErr()
|
||||
}
|
||||
policy.Limit = limit
|
||||
}
|
||||
if paidEnabled, ok := req.GetPaidEnabled(); ok {
|
||||
policy.PaidEnabled = paidEnabled
|
||||
}
|
||||
switch reactions := req.AvailableReactions.(type) {
|
||||
case *tg.ChatReactionsNone:
|
||||
|
|
@ -111,6 +117,7 @@ func domainChannelReactionPolicy(req *tg.MessagesSetChatAvailableReactionsReques
|
|||
if err != nil {
|
||||
return domain.ChannelReactionPolicy{}, tgerr400("REACTION_INVALID")
|
||||
}
|
||||
parsed = normalizeDefaultReactionDocument(parsed, defaultReactionDocuments)
|
||||
key := parsed.Key()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -546,10 +546,10 @@ func tgChannelFull(view domain.ChannelView) *tg.ChannelFull {
|
|||
if ch.ReactionPolicy.Limit > 0 {
|
||||
full.SetReactionsLimit(ch.ReactionPolicy.Limit)
|
||||
}
|
||||
// 付费 reaction(Stars)是广播频道默认能力——官方语义下 channelFull.paid_reactions_available
|
||||
// 对广播频道恒真,客户端据此显示星按钮;megagroup 不支持。与 store 侧
|
||||
// AddChannelMessagePaidReaction 仅广播频道的门槛一致。显式 PaidEnabled 也保留。
|
||||
if (ch.Broadcast && !ch.Megagroup) || ch.ReactionPolicy.PaidEnabled {
|
||||
// paid_reactions_available reflects the saved chat policy, not mere broadcast
|
||||
// capability. Android counts this flag as an extra available reaction in the
|
||||
// settings row, so advertising it without paid_enabled corrupts the UI count.
|
||||
if ch.ReactionPolicy.PaidEnabled {
|
||||
full.SetPaidReactionsAvailable(true)
|
||||
}
|
||||
if ch.Broadcast && !ch.Megagroup {
|
||||
|
|
|
|||
|
|
@ -765,6 +765,11 @@ func messagesSearchFilterMusic(filter tg.MessagesFilterClass) bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
func messagesSearchFilterChatPhotos(filter tg.MessagesFilterClass) bool {
|
||||
_, ok := filter.(*tg.InputMessagesFilterChatPhotos)
|
||||
return ok
|
||||
}
|
||||
|
||||
func searchFilterNeedsMediaStore(filter tg.MessagesFilterClass) bool {
|
||||
switch filter.(type) {
|
||||
case nil, *tg.InputMessagesFilterEmpty:
|
||||
|
|
|
|||
|
|
@ -154,6 +154,61 @@ func TestMessagesSearchChannelPeerReturnsSingleCopyMessages(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessagesSearchChatPhotosDoesNotReturnOrdinaryChannelHistory(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 93500, Phone: "15550093500", FirstName: "Owner"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
channelService := appchannels.NewService(channelStore)
|
||||
r := New(Config{}, Deps{
|
||||
Channels: channelService,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
||||
Title: "Chat Photos Count Guard", Megagroup: true, Date: 1700034000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channel := created.Channel
|
||||
for i := 0; i < 7; i++ {
|
||||
if _, err := channelService.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
||||
ChannelID: channel.ID,
|
||||
RandomID: int64(1700034000 + i),
|
||||
Message: "ordinary channel message",
|
||||
Date: 1700034001 + i,
|
||||
}); err != nil {
|
||||
t.Fatalf("send ordinary message %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
req := &tg.MessagesSearchRequest{
|
||||
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
Filter: &tg.InputMessagesFilterChatPhotos{},
|
||||
Limit: 80,
|
||||
}
|
||||
var in bin.Buffer
|
||||
if err := req.Encode(&in); err != nil {
|
||||
t.Fatalf("encode chat photos search: %v", err)
|
||||
}
|
||||
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &in)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch chat photos search: %v", err)
|
||||
}
|
||||
if box, ok := enc.(*tg.MessagesMessagesBox); ok {
|
||||
enc = box.Messages
|
||||
}
|
||||
got, ok := enc.(*tg.MessagesChannelMessages)
|
||||
if !ok {
|
||||
t.Fatalf("chat photos search result = %T, want messages.channelMessages", enc)
|
||||
}
|
||||
if got.Count != 0 || len(got.Messages) != 0 {
|
||||
t.Fatalf("chat photos search = count %d messages %d, want explicit empty stub", got.Count, len(got.Messages))
|
||||
}
|
||||
if len(got.Chats) != 1 {
|
||||
t.Fatalf("chat photos search chats = %d, want current channel projection", len(got.Chats))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetSearchCountersUsesMediaCategoryCounts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
|
|||
|
|
@ -195,6 +195,34 @@ func TestSendReactionAllowsCustomEmojiFromChannelPolicy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendReactionNormalizesDefaultReactionDocumentID(t *testing.T) {
|
||||
f := newReactionPolicyFixture(t, false)
|
||||
f.router.deps.Files = &fakeFiles{reactions: []domain.AvailableReaction{
|
||||
{Reaction: "\U0001f44d", ActivateAnimationID: 8801},
|
||||
}}
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := f.channelSvc.SetAvailableReactions(ctx, f.ownerID, f.channel.ID, domain.ChannelReactionPolicy{
|
||||
Type: domain.ChannelReactionPolicySome,
|
||||
Emoticons: []string{"\U0001f44d"},
|
||||
}); err != nil {
|
||||
t.Fatalf("set emoji whitelist policy: %v", err)
|
||||
}
|
||||
|
||||
updates, err := f.sendTLReactions(t, f.memberID, &tg.ReactionCustomEmoji{DocumentID: 8801})
|
||||
if err != nil {
|
||||
t.Fatalf("send default reaction by document id: %v", err)
|
||||
}
|
||||
update := reactionUpdateFromUpdates(t, updates)
|
||||
if len(update.Reactions.Results) != 1 {
|
||||
t.Fatalf("reaction results = %+v, want one normalized emoji result", update.Reactions.Results)
|
||||
}
|
||||
emoji, ok := update.Reactions.Results[0].Reaction.(*tg.ReactionEmoji)
|
||||
if !ok || emoji.Emoticon != "\U0001f44d" {
|
||||
t.Fatalf("normalized reaction = %T %+v, want reactionEmoji thumbs up", update.Reactions.Results[0].Reaction, update.Reactions.Results[0].Reaction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReactionEnforcesUniqueReactionsLimit(t *testing.T) {
|
||||
f := newReactionPolicyFixture(t, false)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ func (r *Router) onMessagesSendReaction(ctx context.Context, req *tg.MessagesSen
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reactions = r.normalizeDefaultReactionDocuments(ctx, reactions)
|
||||
// 官方语义(reactions_user_max_default/premium):向量尾部是最新选择,
|
||||
// 超出每用户上限丢弃旧的而非报错;premium viewer 用 premium 档(appConfig
|
||||
// reactions_user_max_premium=3),否则客户端允许的多 reaction 会被静默裁剪。
|
||||
|
|
|
|||
|
|
@ -363,6 +363,21 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
if isLegacyInputPeerChat(req.Peer) {
|
||||
return &tg.MessagesMessages{}, nil
|
||||
}
|
||||
if messagesSearchFilterChatPhotos(req.Filter) {
|
||||
view, err := r.resolveInputPeerChannelView(ctx, userID, req.Peer, filter.Peer.ID)
|
||||
if err != nil {
|
||||
return nil, channelInvalidErr(err)
|
||||
}
|
||||
out := &tg.MessagesChannelMessages{
|
||||
Pts: view.Channel.Pts,
|
||||
Count: 0,
|
||||
Messages: []tg.MessageClass{},
|
||||
Chats: []tg.ChatClass{tgChannelChatForView(userID, view)},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
r.applyStoryMaxIDsToMessages(ctx, userID, out)
|
||||
return out, nil
|
||||
}
|
||||
if searchFilterNeedsMediaStore(req.Filter) {
|
||||
if mediaSearchCountOnlyRequest(req) {
|
||||
view, err := r.resolveInputPeerChannelView(ctx, userID, req.Peer, filter.Peer.ID)
|
||||
|
|
@ -445,6 +460,12 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
}
|
||||
return r.tgMessagesMessages(ctx, userID, r.enrichMessageList(ctx, userID, list)), nil
|
||||
}
|
||||
if messagesSearchFilterChatPhotos(req.Filter) {
|
||||
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.tgMessagesMessages(ctx, userID, domain.MessageList{}), nil
|
||||
}
|
||||
if r.deps.Messages == nil {
|
||||
return messagesNotModifiedOrEmpty(req.Hash), nil
|
||||
}
|
||||
|
|
|
|||
156
internal/rpc/reactions_android_compat.go
Normal file
156
internal/rpc/reactions_android_compat.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// availableReactionDocumentMapCache caches the global reaction catalog mapping.
|
||||
// Returned maps are shared and must be treated as read-only.
|
||||
type availableReactionDocumentMapCache struct {
|
||||
mu sync.RWMutex
|
||||
loaded bool
|
||||
emojiToDocumentID map[string]int64
|
||||
documentIDToEmoji map[int64]string
|
||||
}
|
||||
|
||||
func channelMemberCanChangeInfo(member domain.ChannelMember) bool {
|
||||
return member.Role == domain.ChannelRoleCreator ||
|
||||
(member.Role == domain.ChannelRoleAdmin && member.AdminRights.ChangeInfo)
|
||||
}
|
||||
|
||||
func (r *Router) applyAndroidChannelReactionEditorCompat(ctx context.Context, full *tg.ChannelFull, canChangeInfo bool) {
|
||||
if full == nil || !canChangeInfo || ClientTypeFrom(ctx) != ClientTypeAndroid {
|
||||
return
|
||||
}
|
||||
raw, ok := full.GetAvailableReactions()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
some, ok := raw.(*tg.ChatReactionsSome)
|
||||
if !ok || len(some.Reactions) == 0 {
|
||||
return
|
||||
}
|
||||
emojiToDocumentID, _ := r.availableReactionDocumentMaps(ctx)
|
||||
if len(emojiToDocumentID) == 0 {
|
||||
return
|
||||
}
|
||||
out := &tg.ChatReactionsSome{Reactions: make([]tg.ReactionClass, 0, len(some.Reactions))}
|
||||
changed := false
|
||||
for _, reaction := range some.Reactions {
|
||||
if emoji, ok := reaction.(*tg.ReactionEmoji); ok && emoji != nil {
|
||||
if documentID := emojiToDocumentID[strings.TrimSpace(emoji.Emoticon)]; documentID > 0 {
|
||||
out.Reactions = append(out.Reactions, &tg.ReactionCustomEmoji{DocumentID: documentID})
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
out.Reactions = append(out.Reactions, reaction)
|
||||
}
|
||||
if changed {
|
||||
full.SetAvailableReactions(out)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) normalizeDefaultReactionDocuments(ctx context.Context, reactions []domain.MessageReaction) []domain.MessageReaction {
|
||||
needsCatalog := false
|
||||
for _, reaction := range reactions {
|
||||
if reaction.Type == domain.MessageReactionCustomEmoji && reaction.DocumentID > 0 {
|
||||
needsCatalog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsCatalog {
|
||||
return reactions
|
||||
}
|
||||
_, documentIDToEmoji := r.availableReactionDocumentMaps(ctx)
|
||||
if len(documentIDToEmoji) == 0 {
|
||||
return reactions
|
||||
}
|
||||
out := make([]domain.MessageReaction, 0, len(reactions))
|
||||
seen := make(map[string]struct{}, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
normalized := normalizeDefaultReactionDocument(reaction, documentIDToEmoji)
|
||||
key := normalized.Key()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, normalized)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeDefaultReactionDocument(reaction domain.MessageReaction, documentIDToEmoji map[int64]string) domain.MessageReaction {
|
||||
if reaction.Type != domain.MessageReactionCustomEmoji || reaction.DocumentID <= 0 {
|
||||
return reaction
|
||||
}
|
||||
if emoticon := documentIDToEmoji[reaction.DocumentID]; emoticon != "" {
|
||||
return domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon}
|
||||
}
|
||||
return reaction
|
||||
}
|
||||
|
||||
func (r *Router) availableReactionDocumentMaps(ctx context.Context) (map[string]int64, map[int64]string) {
|
||||
if r == nil || r.deps.Files == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return r.availableReactionDocuments.get(ctx, r.deps.Files)
|
||||
}
|
||||
|
||||
func (c *availableReactionDocumentMapCache) get(ctx context.Context, files FilesService) (map[string]int64, map[int64]string) {
|
||||
if files == nil {
|
||||
return nil, nil
|
||||
}
|
||||
c.mu.RLock()
|
||||
if c.loaded {
|
||||
emojiToDocumentID, documentIDToEmoji := c.emojiToDocumentID, c.documentIDToEmoji
|
||||
c.mu.RUnlock()
|
||||
return emojiToDocumentID, documentIDToEmoji
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.loaded {
|
||||
return c.emojiToDocumentID, c.documentIDToEmoji
|
||||
}
|
||||
catalog, err := files.ListAvailableReactions(ctx)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
c.emojiToDocumentID, c.documentIDToEmoji = buildAvailableReactionDocumentMaps(catalog)
|
||||
c.loaded = true
|
||||
return c.emojiToDocumentID, c.documentIDToEmoji
|
||||
}
|
||||
|
||||
func (c *availableReactionDocumentMapCache) clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.loaded = false
|
||||
c.emojiToDocumentID = nil
|
||||
c.documentIDToEmoji = nil
|
||||
}
|
||||
|
||||
func buildAvailableReactionDocumentMaps(catalog []domain.AvailableReaction) (map[string]int64, map[int64]string) {
|
||||
emojiToDocumentID := make(map[string]int64, len(catalog))
|
||||
documentIDToEmoji := make(map[int64]string, len(catalog))
|
||||
for _, item := range catalog {
|
||||
emoticon := strings.TrimSpace(item.Reaction)
|
||||
if item.Inactive || emoticon == "" || item.ActivateAnimationID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := emojiToDocumentID[emoticon]; !exists {
|
||||
emojiToDocumentID[emoticon] = item.ActivateAnimationID
|
||||
}
|
||||
if _, exists := documentIDToEmoji[item.ActivateAnimationID]; !exists {
|
||||
documentIDToEmoji[item.ActivateAnimationID] = emoticon
|
||||
}
|
||||
}
|
||||
return emojiToDocumentID, documentIDToEmoji
|
||||
}
|
||||
|
|
@ -125,6 +125,7 @@ type Router struct {
|
|||
userFullProjectionCache *userFullProjectionCache
|
||||
peerSettingsProjectionCache *peerSettingsProjectionCache
|
||||
channelFullProjectionCache *channelFullProjectionCache
|
||||
availableReactionDocuments availableReactionDocumentMapCache
|
||||
emojiStickers *emojiStickerIndex
|
||||
notifySettings *notifySettingsCache
|
||||
stickerCatalog *stickerCatalogCache
|
||||
|
|
|
|||
|
|
@ -126,10 +126,11 @@ type channelFullProjectionKey struct {
|
|||
}
|
||||
|
||||
type channelFullProjection struct {
|
||||
accessHash int64
|
||||
full tg.ChannelFull
|
||||
chats []tg.ChatClass
|
||||
userIDs []int64
|
||||
accessHash int64
|
||||
canChangeInfo bool
|
||||
full tg.ChannelFull
|
||||
chats []tg.ChatClass
|
||||
userIDs []int64
|
||||
}
|
||||
|
||||
type channelFullProjectionCache struct {
|
||||
|
|
@ -180,10 +181,11 @@ func (c *channelFullProjectionCache) DeletePair(viewerUserID, channelID int64) {
|
|||
|
||||
func cloneChannelFullProjection(in channelFullProjection) channelFullProjection {
|
||||
return channelFullProjection{
|
||||
accessHash: in.accessHash,
|
||||
full: cloneChannelFull(in.full),
|
||||
chats: cloneChatClasses(in.chats),
|
||||
userIDs: cloneInt64s(in.userIDs),
|
||||
accessHash: in.accessHash,
|
||||
canChangeInfo: in.canChangeInfo,
|
||||
full: cloneChannelFull(in.full),
|
||||
chats: cloneChatClasses(in.chats),
|
||||
userIDs: cloneInt64s(in.userIDs),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue