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

595 lines
21 KiB
Go

package rpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tlprofile"
"telesrv/internal/domain"
)
const (
maxStatsPublicForwardsLimit = 100
maxStatsOffsetLength = 128
maxStatsGraphTokenLength = 128
)
func (r *Router) registerStats(d *tlprofile.Dispatcher) {
registerRPC[*tg.StatsGetBroadcastStatsRequest](d, tlprofile.SemanticMethodStatsGetBroadcastStats, func(ctx context.Context, layerRequest *tg.StatsGetBroadcastStatsRequest) (any, error) {
return r.onStatsGetBroadcastStats(ctx, layerRequest)
})
registerRPC[*tg.StatsGetMegagroupStatsRequest](d, tlprofile.SemanticMethodStatsGetMegagroupStats, func(ctx context.Context, layerRequest *tg.StatsGetMegagroupStatsRequest) (any, error) {
return r.onStatsGetMegagroupStats(ctx, layerRequest)
})
registerRPC[*tg.StatsGetMessageStatsRequest](d, tlprofile.SemanticMethodStatsGetMessageStats, func(ctx context.Context, layerRequest *tg.StatsGetMessageStatsRequest) (any, error) {
return r.onStatsGetMessageStats(ctx, layerRequest)
})
registerRPC[*tg.StatsGetMessagePublicForwardsRequest](d, tlprofile.SemanticMethodStatsGetMessagePublicForwards, func(ctx context.Context, layerRequest *tg.StatsGetMessagePublicForwardsRequest) (any, error) {
return r.onStatsGetMessagePublicForwards(ctx, layerRequest)
})
registerRPC[*tg.StatsLoadAsyncGraphRequest](d, tlprofile.SemanticMethodStatsLoadAsyncGraph, func(ctx context.Context, layerRequest *tg.StatsLoadAsyncGraphRequest) (any, error) {
return r.onStatsLoadAsyncGraph(ctx, layerRequest)
})
registerRPC[*tg.StatsGetStoryStatsRequest](d, tlprofile.SemanticMethodStatsGetStoryStats, func(ctx context.Context, layerRequest *tg.StatsGetStoryStatsRequest) (any, error) {
return r.onStatsGetStoryStats(ctx, layerRequest)
})
registerRPC[*tg.StatsGetStoryPublicForwardsRequest](d, tlprofile.SemanticMethodStatsGetStoryPublicForwards, func(ctx context.Context, layerRequest *tg.StatsGetStoryPublicForwardsRequest) (any, error) {
return r.onStatsGetStoryPublicForwards(ctx, layerRequest)
})
registerRPC[*tg.StatsGetPollStatsRequest](d, tlprofile.SemanticMethodStatsGetPollStats, func(ctx context.Context, layerRequest *tg.StatsGetPollStatsRequest) (any, error) {
return r.onStatsGetPollStats(ctx, layerRequest)
})
}
func (r *Router) onStatsGetBroadcastStats(ctx context.Context, req *tg.StatsGetBroadcastStatsRequest) (*tg.StatsBroadcastStats, error) {
view, err := r.statsChannelView(ctx, req.Channel)
if err != nil {
return nil, err
}
if !view.Channel.Broadcast {
return nil, tgerr400("BROADCAST_REQUIRED")
}
stats, err := r.deps.Channels.GetStats(ctx, view.Self.UserID, domain.ChannelStatsRequest{
ChannelID: view.Channel.ID,
Period: r.statsPeriod(),
})
if err != nil {
return nil, statsServiceErr(err)
}
return r.tgBroadcastStats(stats), nil
}
func (r *Router) onStatsGetMegagroupStats(ctx context.Context, req *tg.StatsGetMegagroupStatsRequest) (*tg.StatsMegagroupStats, error) {
view, err := r.statsChannelView(ctx, req.Channel)
if err != nil {
return nil, err
}
if !view.Channel.Megagroup {
return nil, tgerr400("MEGAGROUP_REQUIRED")
}
stats, err := r.deps.Channels.GetStats(ctx, view.Self.UserID, domain.ChannelStatsRequest{
ChannelID: view.Channel.ID,
Period: r.statsPeriod(),
})
if err != nil {
return nil, statsServiceErr(err)
}
return r.tgMegagroupStats(ctx, view.Self.UserID, stats), nil
}
func (r *Router) onStatsGetMessageStats(ctx context.Context, req *tg.StatsGetMessageStatsRequest) (*tg.StatsMessageStats, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
view, err := r.statsChannelView(ctx, req.Channel)
if err != nil {
return nil, err
}
stats, err := r.deps.Channels.GetMessageStats(ctx, view.Self.UserID, domain.ChannelMessageStatsRequest{
ChannelID: view.Channel.ID,
MessageID: req.MsgID,
Period: r.statsPeriod(),
})
if err != nil {
return nil, statsServiceErr(err)
}
return &tg.StatsMessageStats{
ViewsGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "views", Label: "Views", Color: statsGraphColors[0], Value: func(day domain.ChannelStatsDay) int { return day.Views },
}}),
ReactionsByEmotionGraph: r.statsReactionGraph(stats.Days),
}, nil
}
func (r *Router) onStatsGetMessagePublicForwards(ctx context.Context, req *tg.StatsGetMessagePublicForwardsRequest) (*tg.StatsPublicForwards, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
if req.Limit < 0 || req.Limit > maxStatsPublicForwardsLimit {
return nil, limitInvalidErr()
}
if len(req.Offset) > maxStatsOffsetLength {
return nil, tgerr400("OFFSET_INVALID")
}
view, err := r.statsChannelView(ctx, req.Channel)
if err != nil {
return nil, err
}
limit := req.Limit
if limit <= 0 {
limit = maxStatsPublicForwardsLimit
}
list, err := r.deps.Channels.ListMessagePublicForwards(ctx, view.Self.UserID, domain.ChannelMessagePublicForwardListRequest{
ChannelID: view.Channel.ID,
MessageID: req.MsgID,
Offset: req.Offset,
Limit: limit,
})
if err != nil {
return nil, statsServiceErr(err)
}
views := make([]domain.StoryView, 0, len(list.Messages))
for _, message := range list.Messages {
views = append(views, domain.StoryView{Date: message.Date, PublicForward: &domain.StoryPublicForward{Message: message}})
}
return r.tgStatsPublicForwards(ctx, view.Self.UserID, domain.StoryPublicForwardList{
Count: list.Count, Forwards: views, NextOffset: list.NextOffset,
}), nil
}
func (r *Router) onStatsLoadAsyncGraph(_ context.Context, req *tg.StatsLoadAsyncGraphRequest) (tg.StatsGraphClass, error) {
if len(req.Token) > maxStatsGraphTokenLength {
return &tg.StatsGraphError{Error: "GRAPH_INVALID_RELOAD"}, nil
}
return &tg.StatsGraphError{Error: "GRAPH_INVALID_RELOAD"}, nil
}
func (r *Router) onStatsGetStoryStats(ctx context.Context, req *tg.StatsGetStoryStatsRequest) (*tg.StatsStoryStats, error) {
if req.ID <= 0 || req.ID > domain.MaxStoryID {
return nil, storyIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Stories != nil {
if err := r.deps.Stories.CanViewStoryStats(ctx, userID, peer); err != nil {
return nil, storyErr(err)
}
}
date := int(r.clock.Now().Unix()) - 1
views := 0
reactions := []domain.ChannelMessageReactionCount(nil)
if r.deps.Stories != nil {
list, loadErr := r.deps.Stories.GetStoriesByID(ctx, userID, peer, []int{req.ID}, int(r.clock.Now().Unix()))
if loadErr != nil {
return nil, storyErr(loadErr)
}
for _, story := range list.Stories {
if story.ID != req.ID || story.Owner != peer || story.Deleted {
continue
}
date = story.Date
views = story.Views.ViewsCount
reactions = story.Views.Reactions
break
}
}
return &tg.StatsStoryStats{
ViewsGraph: r.statsSnapshotGraph("views", "Views", date, views),
ReactionsByEmotionGraph: r.statsReactionSnapshotGraph(date, reactions),
}, nil
}
func (r *Router) onStatsGetStoryPublicForwards(ctx context.Context, req *tg.StatsGetStoryPublicForwardsRequest) (*tg.StatsPublicForwards, error) {
if req.ID <= 0 || req.ID > domain.MaxStoryID {
return nil, storyIDInvalidErr()
}
if req.Limit < 0 || req.Limit > maxStatsPublicForwardsLimit || len(req.Offset) > maxStatsOffsetLength {
return nil, limitInvalidErr()
}
if err := domain.ValidateStoryInteractionOffset(req.Offset, false); err != nil {
return nil, storyErr(err)
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if r.deps.Stories == nil || userID == 0 {
return emptyStatsPublicForwards(), nil
}
limit := req.Limit
if limit <= 0 || limit > domain.MaxStoryInteractionListLimit {
limit = domain.MaxStoryInteractionListLimit
}
storyForwards, err := r.deps.Stories.GetStoryPublicForwards(ctx, userID, domain.StoryPublicForwardListRequest{
ViewerUserID: userID,
Owner: peer,
StoryID: req.ID,
Offset: req.Offset,
Limit: limit,
})
if err != nil {
return nil, storyErr(err)
}
messageForwards := domain.StoryMessageForwardList{}
if r.deps.Channels != nil {
messageForwards, err = r.deps.Channels.ListStoryMessageForwards(ctx, userID, domain.StoryMessageForwardListRequest{
ViewerUserID: userID,
Owner: peer,
StoryID: req.ID,
Offset: req.Offset,
Limit: limit,
ForwardsFirst: true,
})
if err != nil {
return nil, storyErr(err)
}
}
hasMore := storyInteractionSourcesHaveMore(len(storyForwards.Forwards), len(messageForwards.Forwards), limit, storyForwards.NextOffset != "" || messageForwards.NextOffset != "")
forwards := mergeStoryInteractionViews(storyForwards.Forwards, messageForwards.Forwards, limit, false, true)
return r.tgStatsPublicForwards(ctx, userID, domain.StoryPublicForwardList{
Count: storyForwards.Count + messageForwards.Count,
Forwards: forwards,
NextOffset: nextStoryInteractionOffset(forwards, limit, false, true, hasMore),
}), nil
}
func (r *Router) onStatsGetPollStats(ctx context.Context, req *tg.StatsGetPollStatsRequest) (*tg.StatsPollStats, error) {
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
date, votes := int(r.clock.Now().Unix())-1, 0
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
history, loadErr := r.deps.Channels.GetMessages(ctx, userID, peer.ID, []int{req.MsgID})
if loadErr != nil {
return nil, channelInvalidErr(loadErr)
}
for _, message := range history.Messages {
if message.ID == req.MsgID && message.Media != nil && message.Media.Poll != nil {
date = message.Date
if message.Media.Poll.Results != nil {
votes = message.Media.Poll.Results.TotalVoters
}
break
}
}
} else if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
messages, loadErr := r.deps.Messages.GetMessages(ctx, userID, []int{req.MsgID})
if loadErr != nil {
return nil, internalErr()
}
for _, message := range messages.Messages {
if message.ID == req.MsgID && message.Peer == peer && message.Media != nil && message.Media.Poll != nil {
date = message.Date
if message.Media.Poll.Results != nil {
votes = message.Media.Poll.Results.TotalVoters
}
break
}
}
}
return &tg.StatsPollStats{VotesGraph: r.statsSnapshotGraph("votes", "Votes", date, votes)}, nil
}
func (r *Router) statsChannelView(ctx context.Context, input tg.InputChannelClass) (domain.ChannelView, error) {
_, view, err := r.channelView(ctx, input)
if err != nil {
return domain.ChannelView{}, err
}
if view.Self.Role != domain.ChannelRoleCreator && view.Self.Role != domain.ChannelRoleAdmin {
return domain.ChannelView{}, tgerr400("CHAT_ADMIN_REQUIRED")
}
return view, nil
}
var statsGraphColors = [...]string{
"#4A90E2", "#50E3C2", "#F5A623", "#D0021B", "#9013FE", "#7ED321", "#B8E986", "#BD10E0",
}
type statsGraphSeries struct {
Key string
Label string
Color string
Value func(domain.ChannelStatsDay) int
}
type statsGraphJSON struct {
Columns [][]any `json:"columns"`
Types map[string]string `json:"types"`
Names map[string]string `json:"names"`
Colors map[string]string `json:"colors"`
}
func (r *Router) statsPeriod() domain.StatsPeriod {
maxDate := int(r.clock.Now().Unix())
if maxDate <= 7*86400 {
maxDate = 7*86400 + 1
}
return domain.StatsPeriod{MinDate: maxDate - 7*86400, MaxDate: maxDate}
}
func tgStatsPeriod(period domain.StatsPeriod) tg.StatsDateRangeDays {
return tg.StatsDateRangeDays{MinDate: period.MinDate, MaxDate: period.MaxDate}
}
func tgStatsValue(value domain.StatsValueAndPrev) tg.StatsAbsValueAndPrev {
return tg.StatsAbsValueAndPrev{Current: value.Current, Previous: value.Previous}
}
func (r *Router) tgBroadcastStats(stats domain.ChannelStats) *tg.StatsBroadcastStats {
recent := make([]tg.PostInteractionCountersClass, 0, len(stats.RecentPosts))
for _, item := range stats.RecentPosts {
recent = append(recent, &tg.PostInteractionCountersMessage{
MsgID: item.MessageID, Views: item.Views, Forwards: item.Forwards, Reactions: item.Reactions,
})
}
return &tg.StatsBroadcastStats{
Period: tgStatsPeriod(stats.Period),
Followers: tgStatsValue(stats.Members),
ViewsPerPost: tgStatsValue(stats.ViewsPerPost),
SharesPerPost: tgStatsValue(stats.SharesPerPost),
ReactionsPerPost: tgStatsValue(stats.ReactionsPerPost),
ViewsPerStory: tg.StatsAbsValueAndPrev{},
SharesPerStory: tg.StatsAbsValueAndPrev{},
ReactionsPerStory: tg.StatsAbsValueAndPrev{},
EnabledNotifications: tg.StatsPercentValue{},
GrowthGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "members", Label: "Followers", Color: statsGraphColors[0], Value: func(day domain.ChannelStatsDay) int { return day.Members },
}}),
FollowersGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "new_members", Label: "New followers", Color: statsGraphColors[1], Value: func(day domain.ChannelStatsDay) int { return day.NewMembers },
}}),
MuteGraph: statsGraphUnavailable(),
TopHoursGraph: statsGraphUnavailable(),
InteractionsGraph: r.statsGraph(stats.Days, []statsGraphSeries{
{Key: "views", Label: "Views", Color: statsGraphColors[0], Value: func(day domain.ChannelStatsDay) int { return day.Views }},
{Key: "shares", Label: "Shares", Color: statsGraphColors[1], Value: func(day domain.ChannelStatsDay) int { return day.Shares }},
}),
IvInteractionsGraph: statsGraphUnavailable(),
ViewsBySourceGraph: statsGraphUnavailable(),
NewFollowersBySourceGraph: statsGraphUnavailable(),
LanguagesGraph: statsGraphUnavailable(),
ReactionsByEmotionGraph: r.statsReactionGraph(stats.Days),
StoryInteractionsGraph: statsGraphUnavailable(),
StoryReactionsByEmotionGraph: statsGraphUnavailable(),
RecentPostsInteractions: recent,
}
}
func (r *Router) tgMegagroupStats(ctx context.Context, viewerUserID int64, stats domain.ChannelStats) *tg.StatsMegagroupStats {
posterIDs := make([]int64, 0, len(stats.TopPosters))
topPosters := make([]tg.StatsGroupTopPoster, 0, len(stats.TopPosters))
for _, item := range stats.TopPosters {
posterIDs = append(posterIDs, item.UserID)
topPosters = append(topPosters, tg.StatsGroupTopPoster{UserID: item.UserID, Messages: item.Messages, AvgChars: item.AvgChars})
}
users := tgUsersForViewer(viewerUserID, r.domainUsersForIDs(ctx, viewerUserID, posterIDs))
out := &tg.StatsMegagroupStats{
Period: tgStatsPeriod(stats.Period),
Members: tgStatsValue(stats.Members),
Messages: tgStatsValue(stats.Messages),
Viewers: tgStatsValue(stats.Viewers),
Posters: tgStatsValue(stats.Posters),
GrowthGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "members", Label: "Members", Color: statsGraphColors[0], Value: func(day domain.ChannelStatsDay) int { return day.Members },
}}),
MembersGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "new_members", Label: "New members", Color: statsGraphColors[1], Value: func(day domain.ChannelStatsDay) int { return day.NewMembers },
}}),
NewMembersBySourceGraph: statsGraphUnavailable(),
LanguagesGraph: statsGraphUnavailable(),
MessagesGraph: r.statsGraph(stats.Days, []statsGraphSeries{{
Key: "messages", Label: "Messages", Color: statsGraphColors[0], Value: func(day domain.ChannelStatsDay) int { return day.Messages },
}}),
ActionsGraph: statsGraphUnavailable(),
TopHoursGraph: statsGraphUnavailable(),
WeekdaysGraph: statsGraphUnavailable(),
TopPosters: topPosters,
TopAdmins: []tg.StatsGroupTopAdmin{},
TopInviters: []tg.StatsGroupTopInviter{},
Users: users,
}
r.applyPeerReadModels(ctx, viewerUserID, out.Users, nil)
return out
}
func (r *Router) statsGraph(days []domain.ChannelStatsDay, series []statsGraphSeries) tg.StatsGraphClass {
graph := statsGraphJSON{
Columns: make([][]any, 0, len(series)+1),
Types: map[string]string{"x": "x"},
Names: make(map[string]string, len(series)),
Colors: make(map[string]string, len(series)),
}
x := make([]any, 1, len(days)+1)
x[0] = "x"
for _, day := range days {
x = append(x, int64(day.Date)*1000)
}
graph.Columns = append(graph.Columns, x)
for i, item := range series {
key := "y" + strconv.Itoa(i)
column := make([]any, 1, len(days)+1)
column[0] = key
for _, day := range days {
column = append(column, item.Value(day))
}
graph.Columns = append(graph.Columns, column)
graph.Types[key] = "line"
graph.Names[key] = item.Label
color := item.Color
if color == "" {
color = statsGraphColors[i%len(statsGraphColors)]
}
graph.Colors[key] = color
}
data, err := json.Marshal(graph)
if err != nil {
return &tg.StatsGraphError{Error: "GRAPH_SERIALIZATION_FAILED"}
}
return &tg.StatsGraph{JSON: tg.DataJSON{Data: string(data)}}
}
func (r *Router) statsReactionGraph(days []domain.ChannelStatsDay) tg.StatsGraphClass {
byKey := make(map[string]domain.MessageReaction)
for _, day := range days {
for _, item := range day.ByReaction {
byKey[item.Reaction.Key()] = item.Reaction
}
}
keys := make([]string, 0, len(byKey))
for key := range byKey {
keys = append(keys, key)
}
sort.Strings(keys)
if len(keys) == 0 {
return r.statsGraph(days, []statsGraphSeries{{
Key: "reactions", Label: "Reactions", Color: statsGraphColors[2], Value: func(day domain.ChannelStatsDay) int { return day.Reactions },
}})
}
series := make([]statsGraphSeries, 0, len(keys))
for i, reactionKey := range keys {
key := reactionKey
reaction := byKey[key]
series = append(series, statsGraphSeries{
Key: key, Label: statsReactionLabel(reaction), Color: statsGraphColors[i%len(statsGraphColors)],
Value: func(day domain.ChannelStatsDay) int {
for _, item := range day.ByReaction {
if item.Reaction.Key() == key {
return item.Count
}
}
return 0
},
})
}
return r.statsGraph(days, series)
}
func statsReactionLabel(reaction domain.MessageReaction) string {
if reaction.Type == domain.MessageReactionCustomEmoji {
return fmt.Sprintf("Custom emoji %d", reaction.DocumentID)
}
if reaction.Emoticon != "" {
return reaction.Emoticon
}
return "Reaction"
}
func (r *Router) statsSnapshotGraph(key, label string, date, value int) tg.StatsGraphClass {
now := int(r.clock.Now().Unix())
if date <= 0 || date >= now {
date = now - 1
}
days := []domain.ChannelStatsDay{{Date: date}, {Date: now, Views: value, Reactions: value}}
return r.statsGraph(days, []statsGraphSeries{{
Key: key, Label: label, Color: statsGraphColors[0],
Value: func(day domain.ChannelStatsDay) int {
if key == "views" {
return day.Views
}
return day.Reactions
},
}})
}
func (r *Router) statsReactionSnapshotGraph(date int, counts []domain.ChannelMessageReactionCount) tg.StatsGraphClass {
now := int(r.clock.Now().Unix())
if date <= 0 || date >= now {
date = now - 1
}
end := domain.ChannelStatsDay{Date: now}
for _, item := range counts {
end.Reactions += item.Count
end.ByReaction = append(end.ByReaction, domain.StatsReactionCount{Reaction: item.Reaction, Count: item.Count})
}
return r.statsReactionGraph([]domain.ChannelStatsDay{{Date: date}, end})
}
func statsGraphUnavailable() tg.StatsGraphClass {
return &tg.StatsGraphError{Error: "GRAPH_NOT_AVAILABLE"}
}
func statsServiceErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrStatsOffsetInvalid):
return tgerr400("OFFSET_INVALID")
default:
return channelInvalidErr(err)
}
}
func emptyStatsPublicForwards() *tg.StatsPublicForwards {
return &tg.StatsPublicForwards{
Count: 0,
Forwards: []tg.PublicForwardClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}
}
func (r *Router) tgStatsPublicForwards(ctx context.Context, viewerUserID int64, list domain.StoryPublicForwardList) *tg.StatsPublicForwards {
users := r.domainUsersForIDs(ctx, viewerUserID, storyViewUserIDs(list.Forwards))
peerUsers, peerChannels := r.storyPeerObjects(ctx, viewerUserID, storyViewPeers(list.Forwards))
users = mergeDomainUsers(users, peerUsers...)
out := &tg.StatsPublicForwards{
Count: list.Count,
Forwards: tgPublicForwardItems(viewerUserID, list.Forwards),
Chats: tgChannels(viewerUserID, peerChannels),
Users: tgUsersForViewer(viewerUserID, users),
}
if list.NextOffset != "" {
out.SetNextOffset(list.NextOffset)
}
r.applyPeerReadModels(ctx, viewerUserID, out.Users, out.Chats)
return out
}
func tgPublicForwardItems(viewerUserID int64, views []domain.StoryView) []tg.PublicForwardClass {
out := make([]tg.PublicForwardClass, 0, len(views))
for _, view := range views {
if view.PublicForward != nil {
msg := tgChannelMessage(viewerUserID, view.PublicForward.Message)
if msg == nil {
continue
}
out = append(out, &tg.PublicForwardMessage{Message: msg})
continue
}
if view.Repost != nil {
peer := tgPeer(view.Repost.Owner)
if peer == nil {
continue
}
out = append(out, &tg.PublicForwardStory{
Peer: peer,
Story: tgStoryItem(*view.Repost),
})
}
}
return out
}