owpengram-server/internal/rpc/story_peer_projection.go
2026-09-01 12:06:31 +03:00

713 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
)
func (r *Router) tgStoriesAllStories(ctx context.Context, viewerUserID int64, list domain.StoryList) tg.StoriesAllStoriesClass {
list = r.withStoryListPeerObjects(ctx, viewerUserID, list)
out := tgStoriesAllStories(viewerUserID, list)
if stories, ok := out.(*tg.StoriesAllStories); ok {
r.applyPeerReadModels(ctx, viewerUserID, stories.Users, stories.Chats)
}
return out
}
func (r *Router) tgStoriesStories(ctx context.Context, viewerUserID int64, list domain.StoryList) *tg.StoriesStories {
list = r.withStoryListPeerObjects(ctx, viewerUserID, list)
out := tgStoriesStories(viewerUserID, list)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) tgStoriesPeerStories(ctx context.Context, viewerUserID int64, peerStories domain.PeerStories) *tg.StoriesPeerStories {
peerStories = r.withPeerStoriesPeerObjects(ctx, viewerUserID, peerStories)
out := tgStoriesPeerStories(viewerUserID, peerStories)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) tgStoryViewsList(ctx context.Context, viewerUserID int64, list domain.StoryViewList) *tg.StoriesStoryViewsList {
users := r.domainUsersForIDs(ctx, viewerUserID, storyViewUserIDs(list.Views))
peerUsers, peerChannels := r.storyPeerObjects(ctx, viewerUserID, storyViewPeers(list.Views))
users = mergeDomainUsers(users, peerUsers...)
out := tgStoryViewsList(viewerUserID, list, users)
if len(peerChannels) > 0 {
out.Chats = appendUniqueTGChats(out.Chats, tgChannels(viewerUserID, peerChannels)...)
}
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) tgStoryReactionsList(ctx context.Context, viewerUserID int64, list domain.StoryReactionList) *tg.StoriesStoryReactionsList {
users := r.domainUsersForIDs(ctx, viewerUserID, storyViewUserIDs(list.Reactions))
peerUsers, peerChannels := r.storyPeerObjects(ctx, viewerUserID, storyViewPeers(list.Reactions))
users = mergeDomainUsers(users, peerUsers...)
out := tgStoryReactionsList(viewerUserID, list, users)
if len(peerChannels) > 0 {
out.Chats = appendUniqueTGChats(out.Chats, tgChannels(viewerUserID, peerChannels)...)
}
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) applyStoryMaxIDsToChats(ctx context.Context, viewerUserID int64, out tg.MessagesChatsClass) tg.MessagesChatsClass {
switch v := out.(type) {
case *tg.MessagesChats:
r.applyPeerReadModels(ctx, viewerUserID, nil, v.Chats)
case *tg.MessagesChatsSlice:
r.applyPeerReadModels(ctx, viewerUserID, nil, v.Chats)
}
return out
}
func (r *Router) tgMessagesDialogs(ctx context.Context, viewerUserID int64, list domain.DialogList) tg.MessagesDialogsClass {
list = r.withDialogNotifySettings(ctx, viewerUserID, list)
out := tgMessagesDialogs(viewerUserID, list)
switch v := out.(type) {
case *tg.MessagesDialogs:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
case *tg.MessagesDialogsSlice:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
}
return out
}
func (r *Router) tgPeerDialogs(ctx context.Context, viewerUserID int64, list domain.DialogList, st domain.UpdateState) *tg.MessagesPeerDialogs {
list = r.withDialogNotifySettings(ctx, viewerUserID, list)
out := tgPeerDialogs(viewerUserID, list, st)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) tgContacts(ctx context.Context, viewerUserID int64, list domain.ContactList) tg.ContactsContactsClass {
out := tgContacts(list)
if contacts, ok := out.(*tg.ContactsContacts); ok {
r.applyPeerReadModels(ctx, viewerUserID, contacts.Users, nil)
}
return out
}
func (r *Router) tgContactsFound(ctx context.Context, viewerUserID int64, res domain.UserSearchResult) *tg.ContactsFound {
out := tgContactsFound(viewerUserID, res)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) tgResolvedUserPeerWithStories(ctx context.Context, viewerUserID int64, u domain.User) *tg.ContactsResolvedPeer {
out := r.tgResolvedUserPeer(viewerUserID, u)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, nil)
return out
}
func (r *Router) tgResolvedChannelPeerWithStories(ctx context.Context, viewerUserID int64, view domain.ChannelView) *tg.ContactsResolvedPeer {
out := tgResolvedChannelPeer(viewerUserID, view)
r.applyPeerReadModels(ctx, viewerUserID, nil, out.Chats)
return out
}
func (r *Router) tgGlobalChannelMessages(ctx context.Context, viewerUserID int64, history domain.ChannelHistory) tg.MessagesMessagesClass {
r.maybeEnqueueExpiredChannelWebPageResolves(viewerUserID, history.Messages)
out := tgGlobalChannelMessages(viewerUserID, history)
r.applyPeerReadModelsToMessages(ctx, viewerUserID, out)
return out
}
func (r *Router) tgMessagesMessages(ctx context.Context, viewerUserID int64, list domain.MessageList) tg.MessagesMessagesClass {
r.maybeEnqueueExpiredPrivateWebPageResolves(list.Messages)
out := tgMessagesMessages(viewerUserID, list)
r.applyPeerReadModelsToMessages(ctx, viewerUserID, out)
return out
}
func (r *Router) tgChannelHistoryMessages(ctx context.Context, viewerUserID int64, history domain.ChannelHistory) tg.MessagesMessagesClass {
r.maybeEnqueueExpiredChannelWebPageResolves(viewerUserID, history.Messages)
out := tgChannelHistoryMessages(viewerUserID, history)
if linked, ok := r.linkedDiscussionChat(ctx, viewerUserID, history.Channel.ID); ok {
switch value := out.(type) {
case *tg.MessagesChannelMessages:
value.Chats = replaceTGChat(value.Chats, linked)
case *tg.MessagesMessagesSlice:
value.Chats = replaceTGChat(value.Chats, linked)
case *tg.MessagesMessages:
value.Chats = replaceTGChat(value.Chats, linked)
}
}
r.applyPeerReadModelsToMessages(ctx, viewerUserID, out)
return out
}
func (r *Router) tgMessagesDiscussionMessage(ctx context.Context, viewerUserID int64, discussion domain.ChannelDiscussionMessage) *tg.MessagesDiscussionMessage {
out := tgMessagesDiscussionMessage(viewerUserID, discussion)
if linked, ok := r.linkedDiscussionChat(ctx, viewerUserID, discussion.PostChannel.ID); ok {
out.Chats = replaceTGChat(out.Chats, linked)
}
// 用带 presence + self 标志的投影覆盖裸 tgUsers防止 viewer 自己以 self=false
// 进入 UsersAndroid putUsers 会覆盖 currentUser
out.Users = r.tgUsersForViewer(viewerUserID, discussion.Users)
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func (r *Router) applyStoryMaxIDsToForumTopics(ctx context.Context, viewerUserID int64, out *tg.MessagesForumTopics) *tg.MessagesForumTopics {
if out != nil {
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
}
return out
}
func (r *Router) applyStoryMaxIDsToMessageViews(ctx context.Context, viewerUserID int64, out *tg.MessagesMessageViews) *tg.MessagesMessageViews {
if out != nil {
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
}
return out
}
func (r *Router) applyStoryMaxIDsToMessageReactionsList(ctx context.Context, viewerUserID int64, out *tg.MessagesMessageReactionsList) *tg.MessagesMessageReactionsList {
if out != nil {
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
}
return out
}
func (r *Router) tgGlobalSearchMessages(ctx context.Context, viewerUserID int64, limit int, private domain.MessageList, channel domain.ChannelHistory) tg.MessagesMessagesClass {
r.maybeEnqueueExpiredPrivateWebPageResolves(private.Messages)
r.maybeEnqueueExpiredChannelWebPageResolves(viewerUserID, channel.Messages)
out := tgGlobalSearchMessages(viewerUserID, limit, private, channel)
r.applyPeerReadModelsToMessages(ctx, viewerUserID, out)
return out
}
// applyPeerReadModelsToMessages stamps every user/channel carried by a messages
// envelope. Supplemental lookups such as messages.getMessages and
// channels.getMessages update the same client-side peer cache as getDialogs, so
// they must use the same response-boundary overlays as history and search.
func (r *Router) applyPeerReadModelsToMessages(ctx context.Context, viewerUserID int64, out tg.MessagesMessagesClass) {
switch v := out.(type) {
case *tg.MessagesMessages:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
case *tg.MessagesMessagesSlice:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
case *tg.MessagesChannelMessages:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
}
}
func (r *Router) tgStoryChangeUpdates(ctx context.Context, viewerUserID int64, peer domain.Peer, story domain.Story, randomID int64, includeStoryID bool, date int) tg.UpdatesClass {
out, _ := tgStoryChangeUpdates(peer, story, randomID, includeStoryID, date).(*tg.Updates)
peers := append([]domain.Peer{peer, story.Owner}, storyForwardPeers(story)...)
out = r.withStoryUpdatePeerObjects(ctx, viewerUserID, out, peers...)
r.appendStoryPrivacyUsers(ctx, viewerUserID, out, []domain.Story{story})
return out
}
func (r *Router) tgStoryReactionUpdates(ctx context.Context, viewerUserID int64, peer domain.Peer, storyID int, reaction tg.ReactionClass, date int) tg.UpdatesClass {
out, _ := tgStoryReactionUpdates(peer, storyID, reaction, date).(*tg.Updates)
return r.withStoryUpdatePeerObjects(ctx, viewerUserID, out, peer)
}
func (r *Router) tgUpdatesDifference(ctx context.Context, viewerUserID int64, diff domain.UpdateDifference) tg.UpdatesDifferenceClass {
out := tgUpdatesDifference(viewerUserID, diff)
switch v := out.(type) {
case *tg.UpdatesDifference:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
case *tg.UpdatesDifferenceSlice:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
}
return out
}
func (r *Router) tgChannelDifference(ctx context.Context, viewerUserID int64, diff domain.ChannelDifference) tg.UpdatesChannelDifferenceClass {
out := tgChannelDifference(viewerUserID, diff)
switch v := out.(type) {
case *tg.UpdatesChannelDifference:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
case *tg.UpdatesChannelDifferenceTooLong:
r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)
}
return out
}
func (r *Router) withStoryUpdatePeerObjects(ctx context.Context, viewerUserID int64, updates *tg.Updates, peers ...domain.Peer) *tg.Updates {
if updates == nil {
return nil
}
users, channels := r.storyPeerObjects(ctx, viewerUserID, peers)
if len(users) > 0 {
updates.Users = appendUniqueTGUsers(updates.Users, r.tgUsersForViewer(viewerUserID, users)...)
}
if len(channels) > 0 {
updates.Chats = appendUniqueTGChats(updates.Chats, tgChannels(viewerUserID, channels)...)
}
r.applyPeerReadModels(ctx, viewerUserID, updates.Users, updates.Chats)
return updates
}
// withStoryUpdatePeerObjectsForOutbox keeps the viewer-specific story overlay
// local to one update while deferring viewer-independent username projection to
// BuildOutboxUpdates' claim-wide pass. This avoids turning story events into an
// extra username-registry query per event before the final batch projection.
func (r *Router) withStoryUpdatePeerObjectsForOutbox(ctx context.Context, viewerUserID int64, updates *tg.Updates, peers ...domain.Peer) *tg.Updates {
return r.withStoryUpdatePeerObjectsForOutboxWithCache(ctx, viewerUserID, updates, newViewerPeerCache(r), peers...)
}
func (r *Router) withStoryUpdatePeerObjectsForOutboxWithCache(ctx context.Context, viewerUserID int64, updates *tg.Updates, cache *viewerPeerCache, peers ...domain.Peer) *tg.Updates {
if updates == nil {
return nil
}
users, channels := r.storyPeerObjectsWithCache(ctx, viewerUserID, cache, peers)
if len(users) > 0 {
projected := tgUsersForViewer(viewerUserID, r.withUsersPresence(users))
updates.Users = appendUniqueTGUsers(updates.Users, projected...)
}
if len(channels) > 0 {
updates.Chats = appendUniqueTGChats(updates.Chats, tgChannels(viewerUserID, channels)...)
}
r.withBotProfileFlagsForUsers(ctx, updates.Users)
r.applyStoryMaxIDsToPeerObjects(ctx, viewerUserID, updates.Users, updates.Chats)
r.applyBotVerificationIconsToPeerObjects(ctx, updates.Users, updates.Chats)
return updates
}
func (r *Router) withStoryListPeerObjects(ctx context.Context, viewerUserID int64, list domain.StoryList) domain.StoryList {
users, channels := r.storyPeerObjects(ctx, viewerUserID, storyListOwnerPeers(list))
if len(users) > 0 {
list.Users = mergeDomainUsers(list.Users, users...)
}
if privacyUsers := r.domainUsersForIDs(ctx, viewerUserID, storyListPrivacyUserIDs(list)); len(privacyUsers) > 0 {
list.Users = mergeDomainUsers(list.Users, privacyUsers...)
}
if len(channels) > 0 {
list.Channels = mergeDomainChannels(list.Channels, channels...)
}
return list
}
func (r *Router) withPeerStoriesPeerObjects(ctx context.Context, viewerUserID int64, peerStories domain.PeerStories) domain.PeerStories {
peers := make([]domain.Peer, 0, len(peerStories.Stories)+1)
peers = append(peers, peerStories.Peer)
for _, story := range peerStories.Stories {
peers = append(peers, story.Owner)
peers = append(peers, storyForwardPeers(story)...)
}
users, channels := r.storyPeerObjects(ctx, viewerUserID, peers)
if len(users) > 0 {
peerStories.Users = mergeDomainUsers(peerStories.Users, users...)
}
if privacyUsers := r.domainUsersForIDs(ctx, viewerUserID, storyPrivacyUserIDs(peerStories.Stories)); len(privacyUsers) > 0 {
peerStories.Users = mergeDomainUsers(peerStories.Users, privacyUsers...)
}
if len(channels) > 0 {
peerStories.Channels = mergeDomainChannels(peerStories.Channels, channels...)
}
return peerStories
}
func (r *Router) appendStoryPrivacyUsers(ctx context.Context, viewerUserID int64, updates *tg.Updates, stories []domain.Story) {
if updates == nil {
return
}
users := r.domainUsersForIDs(ctx, viewerUserID, storyPrivacyUserIDs(stories))
if len(users) == 0 {
return
}
updates.Users = appendUniqueTGUsers(updates.Users, r.tgUsersForViewer(viewerUserID, users)...)
r.applyPeerReadModels(ctx, viewerUserID, updates.Users, updates.Chats)
}
func storyViewUserIDs(views []domain.StoryView) []int64 {
if len(views) == 0 {
return nil
}
seen := make(map[int64]struct{}, len(views))
ids := make([]int64, 0, len(views))
for _, view := range views {
if view.PublicForward != nil {
if media := view.PublicForward.Message.Media; media != nil && media.Kind == domain.MessageMediaKindStory && media.Story != nil && media.Story.Peer.Type == domain.PeerTypeUser && media.Story.Peer.ID != 0 {
if _, ok := seen[media.Story.Peer.ID]; !ok {
seen[media.Story.Peer.ID] = struct{}{}
ids = append(ids, media.Story.Peer.ID)
}
}
if view.PublicForward.Message.SenderUserID != 0 {
if _, ok := seen[view.PublicForward.Message.SenderUserID]; !ok {
seen[view.PublicForward.Message.SenderUserID] = struct{}{}
ids = append(ids, view.PublicForward.Message.SenderUserID)
}
}
if view.PublicForward.Message.From.Type == domain.PeerTypeUser && view.PublicForward.Message.From.ID != 0 {
if _, ok := seen[view.PublicForward.Message.From.ID]; !ok {
seen[view.PublicForward.Message.From.ID] = struct{}{}
ids = append(ids, view.PublicForward.Message.From.ID)
}
}
if view.PublicForward.Message.SendAs != nil && view.PublicForward.Message.SendAs.Type == domain.PeerTypeUser && view.PublicForward.Message.SendAs.ID != 0 {
if _, ok := seen[view.PublicForward.Message.SendAs.ID]; !ok {
seen[view.PublicForward.Message.SendAs.ID] = struct{}{}
ids = append(ids, view.PublicForward.Message.SendAs.ID)
}
}
continue
}
if view.Repost != nil {
continue
}
if view.ViewerID == 0 {
continue
}
if _, ok := seen[view.ViewerID]; ok {
continue
}
seen[view.ViewerID] = struct{}{}
ids = append(ids, view.ViewerID)
}
return ids
}
func storyViewPeers(views []domain.StoryView) []domain.Peer {
if len(views) == 0 {
return nil
}
peers := make([]domain.Peer, 0, len(views))
for _, view := range views {
if view.PublicForward != nil {
if view.PublicForward.Message.ChannelID != 0 {
peers = append(peers, domain.Peer{Type: domain.PeerTypeChannel, ID: view.PublicForward.Message.ChannelID})
}
if media := view.PublicForward.Message.Media; media != nil && media.Kind == domain.MessageMediaKindStory && media.Story != nil && media.Story.Peer.ID != 0 {
peers = append(peers, media.Story.Peer)
}
if view.PublicForward.Message.From.ID != 0 {
peers = append(peers, view.PublicForward.Message.From)
}
if view.PublicForward.Message.SendAs != nil && view.PublicForward.Message.SendAs.ID != 0 {
peers = append(peers, *view.PublicForward.Message.SendAs)
}
continue
}
if view.Repost == nil {
continue
}
peers = append(peers, view.Repost.Owner)
peers = append(peers, storyForwardPeers(*view.Repost)...)
}
return peers
}
func (r *Router) storyPeerObjects(ctx context.Context, viewerUserID int64, peers []domain.Peer) ([]domain.User, []domain.Channel) {
return r.storyPeerObjectsWithCache(ctx, viewerUserID, newViewerPeerCache(r), peers)
}
func (r *Router) storyPeerObjectsWithCache(ctx context.Context, viewerUserID int64, cache *viewerPeerCache, peers []domain.Peer) ([]domain.User, []domain.Channel) {
if viewerUserID == 0 || len(peers) == 0 {
return nil, nil
}
userIDs := make(map[int64]struct{})
channelIDs := make(map[int64]struct{})
for _, peer := range peers {
switch peer.Type {
case domain.PeerTypeUser:
if peer.ID != 0 {
userIDs[peer.ID] = struct{}{}
}
case domain.PeerTypeChannel:
if peer.ID != 0 {
channelIDs[peer.ID] = struct{}{}
}
}
}
if cache == nil {
cache = newViewerPeerCache(r)
}
return cache.usersForIDs(ctx, viewerUserID, mapKeys(userIDs)),
cache.channelsForIDs(ctx, viewerUserID, mapKeys(channelIDs))
}
func storyListOwnerPeers(list domain.StoryList) []domain.Peer {
peers := make([]domain.Peer, 0, len(list.Peers)+len(list.Stories))
for _, item := range list.Peers {
peers = append(peers, item.Peer)
for _, story := range item.Stories {
peers = append(peers, storyForwardPeers(story)...)
}
}
for _, story := range list.Stories {
peers = append(peers, story.Owner)
peers = append(peers, storyForwardPeers(story)...)
}
return peers
}
func storyForwardPeers(story domain.Story) []domain.Peer {
if story.Forward == nil || story.Forward.From.ID == 0 {
return nil
}
switch story.Forward.From.Type {
case domain.PeerTypeUser, domain.PeerTypeChannel:
return []domain.Peer{story.Forward.From}
default:
return nil
}
}
func storyListPrivacyUserIDs(list domain.StoryList) []int64 {
stories := make([]domain.Story, 0, len(list.Stories))
stories = append(stories, list.Stories...)
for _, item := range list.Peers {
stories = append(stories, item.Stories...)
}
return storyPrivacyUserIDs(stories)
}
func storyPrivacyUserIDs(stories []domain.Story) []int64 {
if len(stories) == 0 {
return nil
}
seen := make(map[int64]struct{})
out := make([]int64, 0)
for _, story := range stories {
if !story.Out {
continue
}
for _, id := range privacyRuleUserIDs(story.PrivacyRules) {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
}
return out
}
func storyUpdateEventPeers(event domain.UpdateEvent) []domain.Peer {
switch event.Type {
case domain.UpdateEventStory:
return append([]domain.Peer{event.Peer, event.Story.Owner}, storyForwardPeers(event.Story)...)
case domain.UpdateEventReadStories, domain.UpdateEventSentStoryReaction:
return []domain.Peer{event.Peer}
case domain.UpdateEventNewStoryReaction:
return append([]domain.Peer{event.Peer, event.Story.Owner}, storyForwardPeers(event.Story)...)
default:
return nil
}
}
func appendUniqueTGUsers(base []tg.UserClass, extra ...tg.UserClass) []tg.UserClass {
seen := make(map[int64]struct{}, len(base)+len(extra))
out := make([]tg.UserClass, 0, len(base)+len(extra))
appendOne := func(item tg.UserClass) {
user, ok := item.(*tg.User)
if !ok || user.ID == 0 {
out = append(out, item)
return
}
if _, exists := seen[user.ID]; exists {
return
}
seen[user.ID] = struct{}{}
out = append(out, item)
}
for _, item := range base {
appendOne(item)
}
for _, item := range extra {
appendOne(item)
}
return out
}
func appendUniqueTGChats(base []tg.ChatClass, extra ...tg.ChatClass) []tg.ChatClass {
seen := make(map[int64]struct{}, len(base)+len(extra))
out := make([]tg.ChatClass, 0, len(base)+len(extra))
appendOne := func(item tg.ChatClass) {
channel, ok := item.(*tg.Channel)
if !ok || channel.ID == 0 {
out = append(out, item)
return
}
if _, exists := seen[channel.ID]; exists {
return
}
seen[channel.ID] = struct{}{}
out = append(out, item)
}
for _, item := range base {
appendOne(item)
}
for _, item := range extra {
appendOne(item)
}
return out
}
// applyPeerReadModels is the response-boundary overlay pass for peer objects that
// have already been projected. Every read model it drives is batched over the
// whole user/chat set, so a handler pays one call per read model per response
// rather than one per peer.
//
// It is the shared hook for handlers that return complete peer envelopes. New
// builders still have to call it explicitly at their response boundary; nested
// single-peer fields and fan-out builders use the corresponding narrow/preload
// helpers instead.
func (r *Router) applyPeerReadModels(ctx context.Context, viewerUserID int64, users []tg.UserClass, chats []tg.ChatClass) {
r.applyStoryMaxIDsToPeerObjects(ctx, viewerUserID, users, chats)
r.applyPeerIdentitiesToPeerObjects(ctx, users, chats)
}
func (r *Router) applyPeerIdentitiesToPeerObjects(ctx context.Context, users []tg.UserClass, chats []tg.ChatClass) {
if len(users)+len(chats) == 0 || (r.deps.Usernames == nil && r.deps.BotVerifications == nil) {
return
}
peers := make([]domain.Peer, 0, len(users)+len(chats))
seen := make(map[domain.Peer]struct{}, len(users)+len(chats))
peers = appendUsernameProjectionPeers(peers, seen, users, chats)
if len(peers) == 0 {
return
}
usernames, verifications := r.peerIdentityMaps(ctx, peers, r.deps.Usernames != nil, r.deps.BotVerifications != nil)
if len(usernames) > 0 {
applyUsernamesFromRegistry(users, chats, usernames)
}
if len(verifications) > 0 {
applyBotVerificationIconsFromMap(users, chats, verifications)
}
}
func (r *Router) applyStoryMaxIDsToPeerObjects(ctx context.Context, viewerUserID int64, users []tg.UserClass, chats []tg.ChatClass) {
if r.deps.Stories == nil || viewerUserID == 0 || len(users)+len(chats) == 0 {
return
}
peers := make([]domain.Peer, 0, len(users)+len(chats))
seen := make(map[domain.Peer]struct{}, len(users)+len(chats))
addPeer := func(peer domain.Peer) {
if peer.ID == 0 {
return
}
if _, ok := seen[peer]; ok {
return
}
seen[peer] = struct{}{}
peers = append(peers, peer)
}
for _, item := range users {
if u, ok := item.(*tg.User); ok && u != nil && !u.Deleted {
addPeer(domain.Peer{Type: domain.PeerTypeUser, ID: u.ID})
}
}
for _, item := range chats {
if ch, ok := item.(*tg.Channel); ok {
addPeer(domain.Peer{Type: domain.PeerTypeChannel, ID: ch.ID})
}
}
recent, hidden := r.storyProjectionMaps(ctx, viewerUserID, peers)
for _, item := range users {
u, ok := item.(*tg.User)
if !ok || u == nil || u.Deleted {
continue
}
peer := domain.Peer{Type: domain.PeerTypeUser, ID: u.ID}
if story, ok := recent[peer]; ok {
u.SetStoriesMaxID(story)
}
if state, ok := hidden[peer]; ok {
u.SetStoriesHidden(state)
}
}
for _, item := range chats {
ch, ok := item.(*tg.Channel)
if !ok {
continue
}
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: ch.ID}
if story, ok := recent[peer]; ok {
ch.SetStoriesMaxID(story)
}
if state, ok := hidden[peer]; ok {
ch.SetStoriesHidden(state)
}
}
}
func (r *Router) applyStoriesPinnedAvailableToUserFull(ctx context.Context, viewerUserID, ownerUserID int64, full *tg.UserFull) {
if full != nil && r.storyPinnedAvailable(ctx, viewerUserID, domain.Peer{Type: domain.PeerTypeUser, ID: ownerUserID}) {
full.SetStoriesPinnedAvailable(true)
}
}
func (r *Router) applyStoriesPinnedAvailableToChannelFull(ctx context.Context, viewerUserID int64, channelID int64, full *tg.ChannelFull) {
if full != nil && r.storyPinnedAvailable(ctx, viewerUserID, domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}) {
full.SetStoriesPinnedAvailable(true)
}
}
func (r *Router) storyPinnedAvailable(ctx context.Context, viewerUserID int64, peer domain.Peer) bool {
if r.deps.Stories == nil || viewerUserID == 0 || peer.ID == 0 {
return false
}
available, err := r.storyPinnedCache.getOrLoad(ctx, viewerUserID, peer, func() (bool, error) {
return r.deps.Stories.HasPinnedStories(ctx, viewerUserID, peer, int(r.clock.Now().Unix()))
})
if err != nil {
r.log.Warn("project story pinned availability failed",
zap.Int64("viewer_user_id", viewerUserID),
zap.String("peer_type", string(peer.Type)),
zap.Int64("peer_id", peer.ID),
zap.Error(err))
return false
}
return available
}
func (r *Router) storyProjectionFreshMaps(ctx context.Context, viewerUserID int64, peers []domain.Peer) (map[domain.Peer]tg.RecentStory, map[domain.Peer]bool) {
if r.deps.Stories == nil || viewerUserID == 0 || len(peers) == 0 {
return nil, nil
}
recent := make(map[domain.Peer]tg.RecentStory, len(peers))
hidden := make(map[domain.Peer]bool, len(peers))
now := int(r.clock.Now().Unix())
for start := 0; start < len(peers); start += domain.MaxStoryIDs {
end := start + domain.MaxStoryIDs
if end > len(peers) {
end = len(peers)
}
chunk := peers[start:end]
for _, peer := range chunk {
hidden[peer] = false
}
projections, err := r.deps.Stories.GetPeerStoryProjections(ctx, viewerUserID, chunk, now)
if err != nil {
r.log.Warn("project story peer summaries failed",
zap.Int64("viewer_user_id", viewerUserID),
zap.Int("peer_count", len(chunk)),
zap.Error(err))
continue
}
for _, item := range projections {
if story, ok := tgRecentStory(item.Recent); ok {
recent[item.Peer] = story
}
hidden[item.Peer] = item.Hidden
}
}
return recent, hidden
}
func tgRecentStory(in domain.RecentStory) (tg.RecentStory, bool) {
if in.MaxID <= 0 && !in.Live {
return tg.RecentStory{}, false
}
out := tg.RecentStory{Live: in.Live}
if in.MaxID > 0 {
out.SetMaxID(in.MaxID)
}
return out, true
}