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

169 lines
4.7 KiB
Go

package domain
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
// MaxChannelStatsDays bounds every aggregate query independently of the
// client request. The RPC currently requests seven days, while keeping a
// small domain ceiling makes future callers safe by construction.
MaxChannelStatsDays = 31
// MaxChannelStatsTopPosters bounds the user hydration work at the RPC edge.
MaxChannelStatsTopPosters = 10
// MaxChannelStatsRecentPosts bounds correlated interaction aggregation.
MaxChannelStatsRecentPosts = 10
// MaxChannelMessagePublicForwards is Telegram's public-forwards page cap.
MaxChannelMessagePublicForwards = 100
)
var ErrStatsOffsetInvalid = errors.New("stats offset invalid")
// StatsPeriod is a half-open Unix-second range [MinDate, MaxDate). Previous
// values always use the equally sized range immediately before MinDate.
type StatsPeriod struct {
MinDate int
MaxDate int
}
func (p StatsPeriod) Valid() bool {
return p.MinDate > 0 && p.MaxDate > p.MinDate &&
p.MaxDate-p.MinDate <= MaxChannelStatsDays*86400
}
func (p StatsPeriod) PreviousMinDate() int {
return p.MinDate - (p.MaxDate - p.MinDate)
}
// StatsValueAndPrev is one current-period value and its previous-period peer.
type StatsValueAndPrev struct {
Current float64
Previous float64
}
// StatsReactionCount is one protocol-neutral reaction series value.
type StatsReactionCount struct {
Reaction MessageReaction
Count int
}
// ChannelStatsDay is a UTC-day bucket. Date is the bucket start.
type ChannelStatsDay struct {
Date int
Members int
NewMembers int
Messages int
Viewers int
Posters int
Views int
Shares int
Reactions int
ByReaction []StatsReactionCount
}
type ChannelStatsTopPoster struct {
UserID int64
Messages int
AvgChars int
}
type ChannelStatsRecentPost struct {
MessageID int
Views int
Forwards int
Reactions int
}
// ChannelStats is the durable minimum needed by broadcast/megagroup stats.
// Unsupported Telegram dimensions (language, notification mute, IV sources)
// are deliberately absent so the RPC edge can return statsGraphError instead
// of manufacturing zero-valued facts.
type ChannelStats struct {
Channel Channel
Period StatsPeriod
Members StatsValueAndPrev
Messages StatsValueAndPrev
Viewers StatsValueAndPrev
Posters StatsValueAndPrev
ViewsPerPost StatsValueAndPrev
SharesPerPost StatsValueAndPrev
ReactionsPerPost StatsValueAndPrev
Days []ChannelStatsDay
TopPosters []ChannelStatsTopPoster
RecentPosts []ChannelStatsRecentPost
}
type ChannelStatsRequest struct {
ViewerUserID int64
ChannelID int64
Period StatsPeriod
}
// ChannelMessageStats contains event-time view and reaction buckets for one
// existing channel message.
type ChannelMessageStats struct {
Channel Channel
Message ChannelMessage
Period StatsPeriod
Days []ChannelStatsDay
}
type ChannelMessageStatsRequest struct {
ViewerUserID int64
ChannelID int64
MessageID int
Period StatsPeriod
}
// ChannelMessagePublicForwardListRequest pages public channel/supergroup
// messages whose forward header identifies one exact source channel post.
type ChannelMessagePublicForwardListRequest struct {
ViewerUserID int64
ChannelID int64
MessageID int
Offset string
Limit int
}
type ChannelMessagePublicForwardList struct {
Count int
Messages []ChannelMessage
NextOffset string
}
// ChannelMessagePublicForwardCursor is ordered by date DESC, destination
// channel ASC, message id DESC. A versioned textual form is intentionally
// opaque to clients while staying easy to validate and log.
type ChannelMessagePublicForwardCursor struct {
Date int
ChannelID int64
MessageID int
}
func ParseChannelMessagePublicForwardCursor(offset string) (ChannelMessagePublicForwardCursor, error) {
if offset == "" {
return ChannelMessagePublicForwardCursor{}, nil
}
parts := strings.Split(offset, ":")
if len(parts) != 4 || parts[0] != "cmf1" {
return ChannelMessagePublicForwardCursor{}, ErrStatsOffsetInvalid
}
date, err1 := strconv.Atoi(parts[1])
channelID, err2 := strconv.ParseInt(parts[2], 10, 64)
messageID, err3 := strconv.Atoi(parts[3])
if err1 != nil || err2 != nil || err3 != nil || date <= 0 || channelID <= 0 ||
messageID <= 0 || messageID > MaxMessageBoxID {
return ChannelMessagePublicForwardCursor{}, ErrStatsOffsetInvalid
}
return ChannelMessagePublicForwardCursor{Date: date, ChannelID: channelID, MessageID: messageID}, nil
}
func FormatChannelMessagePublicForwardCursor(message ChannelMessage) string {
if message.Date <= 0 || message.ChannelID <= 0 || message.ID <= 0 {
return ""
}
return fmt.Sprintf("cmf1:%d:%d:%d", message.Date, message.ChannelID, message.ID)
}