392 lines
13 KiB
Go
392 lines
13 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func (s *ChannelStore) GetChannelStats(_ context.Context, req domain.ChannelStatsRequest) (domain.ChannelStats, error) {
|
|
if req.ViewerUserID == 0 || req.ChannelID == 0 || !req.Period.Valid() {
|
|
return domain.ChannelStats{}, domain.ErrChannelInvalid
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
channel, _, err := s.statsAdminChannelLocked(req.ViewerUserID, req.ChannelID)
|
|
if err != nil {
|
|
return domain.ChannelStats{}, err
|
|
}
|
|
|
|
stats := domain.ChannelStats{Channel: cloneChannel(channel), Period: req.Period}
|
|
days, dayIndex := newMemoryStatsDays(req.Period)
|
|
prevMin := req.Period.PreviousMinDate()
|
|
var currentMessages, previousMessages int
|
|
var currentViews, previousViews int
|
|
currentPosters := make(map[int64]struct{})
|
|
previousPosters := make(map[int64]struct{})
|
|
currentMessageDay := make(map[int]int)
|
|
previousMessageIDs := make(map[int]struct{})
|
|
currentMessageIDs := make(map[int]struct{})
|
|
top := make(map[int64]struct{ messages, chars int })
|
|
|
|
for _, member := range s.members[req.ChannelID] {
|
|
if memoryStatsMemberActiveAt(member, req.Period.MaxDate-1) {
|
|
stats.Members.Current++
|
|
}
|
|
if memoryStatsMemberActiveAt(member, req.Period.MinDate-1) {
|
|
stats.Members.Previous++
|
|
}
|
|
if i, ok := dayIndex[memoryStatsDay(member.JoinedAt)]; ok && member.JoinedAt >= req.Period.MinDate && member.JoinedAt < req.Period.MaxDate {
|
|
days[i].NewMembers++
|
|
}
|
|
}
|
|
for i := range days {
|
|
at := days[i].Date + 86400 - 1
|
|
if at >= req.Period.MaxDate {
|
|
at = req.Period.MaxDate - 1
|
|
}
|
|
for _, member := range s.members[req.ChannelID] {
|
|
if memoryStatsMemberActiveAt(member, at) {
|
|
days[i].Members++
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, msg := range s.messages[req.ChannelID] {
|
|
if msg.Deleted || msg.Action != nil {
|
|
continue
|
|
}
|
|
switch {
|
|
case msg.Date >= req.Period.MinDate && msg.Date < req.Period.MaxDate:
|
|
currentMessages++
|
|
currentViews += msg.ViewsCount
|
|
currentPosters[msg.SenderUserID] = struct{}{}
|
|
currentMessageIDs[msg.ID] = struct{}{}
|
|
if i, ok := dayIndex[memoryStatsDay(msg.Date)]; ok {
|
|
currentMessageDay[msg.ID] = i
|
|
days[i].Messages++
|
|
days[i].Views += msg.ViewsCount
|
|
}
|
|
entry := top[msg.SenderUserID]
|
|
entry.messages++
|
|
entry.chars += utf8.RuneCountInString(msg.Body)
|
|
top[msg.SenderUserID] = entry
|
|
case msg.Date >= prevMin && msg.Date < req.Period.MinDate:
|
|
previousMessages++
|
|
previousViews += msg.ViewsCount
|
|
previousPosters[msg.SenderUserID] = struct{}{}
|
|
previousMessageIDs[msg.ID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
currentViewerIDs := make(map[int64]struct{})
|
|
previousViewerIDs := make(map[int64]struct{})
|
|
dayViewerIDs := make(map[int]map[int64]struct{}, len(days))
|
|
for _, viewers := range s.msgViewers[req.ChannelID] {
|
|
for userID, viewedAt := range viewers {
|
|
switch {
|
|
case viewedAt >= req.Period.MinDate && viewedAt < req.Period.MaxDate:
|
|
currentViewerIDs[userID] = struct{}{}
|
|
if i, ok := dayIndex[memoryStatsDay(viewedAt)]; ok {
|
|
if dayViewerIDs[i] == nil {
|
|
dayViewerIDs[i] = make(map[int64]struct{})
|
|
}
|
|
dayViewerIDs[i][userID] = struct{}{}
|
|
}
|
|
case viewedAt >= prevMin && viewedAt < req.Period.MinDate:
|
|
previousViewerIDs[userID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
var currentReactions, previousReactions int
|
|
for messageID, byUser := range s.reactions[req.ChannelID] {
|
|
_, current := currentMessageIDs[messageID]
|
|
_, previous := previousMessageIDs[messageID]
|
|
for _, rows := range byUser {
|
|
for _, row := range rows {
|
|
if current {
|
|
currentReactions++
|
|
if i, ok := currentMessageDay[messageID]; ok {
|
|
days[i].Reactions++
|
|
addMemoryStatsReaction(&days[i], row.Reaction)
|
|
}
|
|
} else if previous {
|
|
previousReactions++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
forwardCounts := s.publicForwardCountsLocked(req.ChannelID)
|
|
currentShares, previousShares := 0, 0
|
|
for messageID, count := range forwardCounts {
|
|
if _, ok := currentMessageIDs[messageID]; ok {
|
|
currentShares += count
|
|
if i, ok := currentMessageDay[messageID]; ok {
|
|
days[i].Shares += count
|
|
}
|
|
} else if _, ok := previousMessageIDs[messageID]; ok {
|
|
previousShares += count
|
|
}
|
|
}
|
|
|
|
for i := range days {
|
|
days[i].Viewers = len(dayViewerIDs[i])
|
|
posters := make(map[int64]struct{})
|
|
start, end := days[i].Date, days[i].Date+86400
|
|
for _, msg := range s.messages[req.ChannelID] {
|
|
if !msg.Deleted && msg.Action == nil && msg.Date >= start && msg.Date < end {
|
|
posters[msg.SenderUserID] = struct{}{}
|
|
}
|
|
}
|
|
days[i].Posters = len(posters)
|
|
sort.Slice(days[i].ByReaction, func(a, b int) bool {
|
|
return days[i].ByReaction[a].Reaction.Key() < days[i].ByReaction[b].Reaction.Key()
|
|
})
|
|
}
|
|
|
|
stats.Messages = domain.StatsValueAndPrev{Current: float64(currentMessages), Previous: float64(previousMessages)}
|
|
stats.Viewers = domain.StatsValueAndPrev{Current: float64(len(currentViewerIDs)), Previous: float64(len(previousViewerIDs))}
|
|
stats.Posters = domain.StatsValueAndPrev{Current: float64(len(currentPosters)), Previous: float64(len(previousPosters))}
|
|
stats.ViewsPerPost = memoryStatsAverage(currentViews, currentMessages, previousViews, previousMessages)
|
|
stats.SharesPerPost = memoryStatsAverage(currentShares, currentMessages, previousShares, previousMessages)
|
|
stats.ReactionsPerPost = memoryStatsAverage(currentReactions, currentMessages, previousReactions, previousMessages)
|
|
stats.Days = days
|
|
|
|
for userID, entry := range top {
|
|
if userID == 0 || entry.messages == 0 {
|
|
continue
|
|
}
|
|
stats.TopPosters = append(stats.TopPosters, domain.ChannelStatsTopPoster{
|
|
UserID: userID, Messages: entry.messages, AvgChars: entry.chars / entry.messages,
|
|
})
|
|
}
|
|
sort.Slice(stats.TopPosters, func(i, j int) bool {
|
|
if stats.TopPosters[i].Messages != stats.TopPosters[j].Messages {
|
|
return stats.TopPosters[i].Messages > stats.TopPosters[j].Messages
|
|
}
|
|
return stats.TopPosters[i].UserID < stats.TopPosters[j].UserID
|
|
})
|
|
if len(stats.TopPosters) > domain.MaxChannelStatsTopPosters {
|
|
stats.TopPosters = stats.TopPosters[:domain.MaxChannelStatsTopPosters]
|
|
}
|
|
|
|
messages := append([]domain.ChannelMessage(nil), s.messages[req.ChannelID]...)
|
|
sort.Slice(messages, func(i, j int) bool {
|
|
if messages[i].Date != messages[j].Date {
|
|
return messages[i].Date > messages[j].Date
|
|
}
|
|
return messages[i].ID > messages[j].ID
|
|
})
|
|
for _, msg := range messages {
|
|
if msg.Deleted || msg.Action != nil {
|
|
continue
|
|
}
|
|
stats.RecentPosts = append(stats.RecentPosts, domain.ChannelStatsRecentPost{
|
|
MessageID: msg.ID,
|
|
Views: msg.ViewsCount,
|
|
Forwards: forwardCounts[msg.ID],
|
|
Reactions: memoryStatsReactionCount(s.reactions[req.ChannelID][msg.ID]),
|
|
})
|
|
if len(stats.RecentPosts) == domain.MaxChannelStatsRecentPosts {
|
|
break
|
|
}
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func (s *ChannelStore) GetChannelMessageStats(_ context.Context, req domain.ChannelMessageStatsRequest) (domain.ChannelMessageStats, error) {
|
|
if req.ViewerUserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 ||
|
|
req.MessageID > domain.MaxMessageBoxID || !req.Period.Valid() {
|
|
return domain.ChannelMessageStats{}, domain.ErrMessageIDInvalid
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
channel, _, err := s.statsAdminChannelLocked(req.ViewerUserID, req.ChannelID)
|
|
if err != nil {
|
|
return domain.ChannelMessageStats{}, err
|
|
}
|
|
message, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
|
|
if !ok || message.Deleted {
|
|
return domain.ChannelMessageStats{}, domain.ErrMessageIDInvalid
|
|
}
|
|
days, dayIndex := newMemoryStatsDays(req.Period)
|
|
for _, viewedAt := range s.msgViewers[req.ChannelID][req.MessageID] {
|
|
if i, ok := dayIndex[memoryStatsDay(viewedAt)]; ok && viewedAt >= req.Period.MinDate && viewedAt < req.Period.MaxDate {
|
|
days[i].Views++
|
|
}
|
|
}
|
|
for _, rows := range s.reactions[req.ChannelID][req.MessageID] {
|
|
for _, row := range rows {
|
|
if i, ok := dayIndex[memoryStatsDay(row.Date)]; ok && row.Date >= req.Period.MinDate && row.Date < req.Period.MaxDate {
|
|
days[i].Reactions++
|
|
addMemoryStatsReaction(&days[i], row.Reaction)
|
|
}
|
|
}
|
|
}
|
|
for i := range days {
|
|
sort.Slice(days[i].ByReaction, func(a, b int) bool {
|
|
return days[i].ByReaction[a].Reaction.Key() < days[i].ByReaction[b].Reaction.Key()
|
|
})
|
|
}
|
|
return domain.ChannelMessageStats{
|
|
Channel: cloneChannel(channel), Message: cloneChannelMessage(message), Period: req.Period, Days: days,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ChannelStore) ListChannelMessagePublicForwards(_ context.Context, req domain.ChannelMessagePublicForwardListRequest) (domain.ChannelMessagePublicForwardList, error) {
|
|
if req.ViewerUserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 ||
|
|
req.MessageID > domain.MaxMessageBoxID || req.Limit <= 0 || req.Limit > domain.MaxChannelMessagePublicForwards {
|
|
return domain.ChannelMessagePublicForwardList{}, domain.ErrChannelInvalid
|
|
}
|
|
cursor, err := domain.ParseChannelMessagePublicForwardCursor(req.Offset)
|
|
if err != nil {
|
|
return domain.ChannelMessagePublicForwardList{}, err
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
if _, _, err := s.statsAdminChannelLocked(req.ViewerUserID, req.ChannelID); err != nil {
|
|
return domain.ChannelMessagePublicForwardList{}, err
|
|
}
|
|
source, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
|
|
if !ok || source.Deleted {
|
|
return domain.ChannelMessagePublicForwardList{}, domain.ErrMessageIDInvalid
|
|
}
|
|
all := make([]domain.ChannelMessage, 0)
|
|
for channelID, channel := range s.channels {
|
|
if !memoryStatsPublicChannel(channel) {
|
|
continue
|
|
}
|
|
for _, msg := range s.messages[channelID] {
|
|
if memoryStatsForwardsPost(msg, req.ChannelID, req.MessageID) {
|
|
all = append(all, cloneChannelMessage(msg))
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(all, func(i, j int) bool { return memoryStatsForwardBefore(all[i], all[j]) })
|
|
page := make([]domain.ChannelMessage, 0, req.Limit)
|
|
for _, msg := range all {
|
|
if cursor.Date != 0 && !memoryStatsForwardAfterCursor(msg, cursor) {
|
|
continue
|
|
}
|
|
page = append(page, msg)
|
|
if len(page) == req.Limit+1 {
|
|
break
|
|
}
|
|
}
|
|
next := ""
|
|
if len(page) > req.Limit {
|
|
page = page[:req.Limit]
|
|
next = domain.FormatChannelMessagePublicForwardCursor(page[len(page)-1])
|
|
}
|
|
return domain.ChannelMessagePublicForwardList{Count: len(all), Messages: page, NextOffset: next}, nil
|
|
}
|
|
|
|
func (s *ChannelStore) statsAdminChannelLocked(userID, channelID int64) (domain.Channel, domain.ChannelMember, error) {
|
|
channel, member, err := s.channelAndMemberLocked(userID, channelID)
|
|
if err != nil {
|
|
return domain.Channel{}, domain.ChannelMember{}, err
|
|
}
|
|
if member.Role != domain.ChannelRoleCreator && member.Role != domain.ChannelRoleAdmin {
|
|
return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelAdminRequired
|
|
}
|
|
return channel, member, nil
|
|
}
|
|
|
|
func newMemoryStatsDays(period domain.StatsPeriod) ([]domain.ChannelStatsDay, map[int]int) {
|
|
start := memoryStatsDay(period.MinDate)
|
|
end := memoryStatsDay(period.MaxDate - 1)
|
|
days := make([]domain.ChannelStatsDay, 0, (end-start)/86400+1)
|
|
index := make(map[int]int)
|
|
for date := start; date <= end; date += 86400 {
|
|
index[date] = len(days)
|
|
days = append(days, domain.ChannelStatsDay{Date: date})
|
|
}
|
|
return days, index
|
|
}
|
|
|
|
func memoryStatsDay(date int) int {
|
|
if date <= 0 {
|
|
return 0
|
|
}
|
|
return date - date%86400
|
|
}
|
|
|
|
func memoryStatsMemberActiveAt(member domain.ChannelMember, at int) bool {
|
|
return member.JoinedAt > 0 && member.JoinedAt <= at && (member.LeftAt == 0 || member.LeftAt > at)
|
|
}
|
|
|
|
func memoryStatsAverage(current, currentCount, previous, previousCount int) domain.StatsValueAndPrev {
|
|
var out domain.StatsValueAndPrev
|
|
if currentCount > 0 {
|
|
out.Current = float64(current) / float64(currentCount)
|
|
}
|
|
if previousCount > 0 {
|
|
out.Previous = float64(previous) / float64(previousCount)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addMemoryStatsReaction(day *domain.ChannelStatsDay, reaction domain.MessageReaction) {
|
|
for i := range day.ByReaction {
|
|
if day.ByReaction[i].Reaction.Key() == reaction.Key() {
|
|
day.ByReaction[i].Count++
|
|
return
|
|
}
|
|
}
|
|
day.ByReaction = append(day.ByReaction, domain.StatsReactionCount{Reaction: reaction, Count: 1})
|
|
}
|
|
|
|
func memoryStatsReactionCount(byUser map[int64][]domain.ChannelMessagePeerReaction) int {
|
|
count := 0
|
|
for _, rows := range byUser {
|
|
count += len(rows)
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (s *ChannelStore) publicForwardCountsLocked(sourceChannelID int64) map[int]int {
|
|
counts := make(map[int]int)
|
|
for channelID, channel := range s.channels {
|
|
if !memoryStatsPublicChannel(channel) {
|
|
continue
|
|
}
|
|
for _, msg := range s.messages[channelID] {
|
|
if msg.Deleted || msg.Forward == nil || msg.Forward.From.Type != domain.PeerTypeChannel ||
|
|
msg.Forward.From.ID != sourceChannelID || msg.Forward.ChannelPost <= 0 {
|
|
continue
|
|
}
|
|
counts[msg.Forward.ChannelPost]++
|
|
}
|
|
}
|
|
return counts
|
|
}
|
|
|
|
func memoryStatsPublicChannel(channel domain.Channel) bool {
|
|
return !channel.Deleted && strings.TrimSpace(channel.Username) != "" && (channel.Broadcast || channel.Megagroup)
|
|
}
|
|
|
|
func memoryStatsForwardsPost(msg domain.ChannelMessage, channelID int64, messageID int) bool {
|
|
return !msg.Deleted && msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeChannel &&
|
|
msg.Forward.From.ID == channelID && msg.Forward.ChannelPost == messageID
|
|
}
|
|
|
|
func memoryStatsForwardBefore(a, b domain.ChannelMessage) bool {
|
|
if a.Date != b.Date {
|
|
return a.Date > b.Date
|
|
}
|
|
if a.ChannelID != b.ChannelID {
|
|
return a.ChannelID < b.ChannelID
|
|
}
|
|
return a.ID > b.ID
|
|
}
|
|
|
|
func memoryStatsForwardAfterCursor(msg domain.ChannelMessage, cursor domain.ChannelMessagePublicForwardCursor) bool {
|
|
return msg.Date < cursor.Date ||
|
|
(msg.Date == cursor.Date && (msg.ChannelID > cursor.ChannelID ||
|
|
(msg.ChannelID == cursor.ChannelID && msg.ID < cursor.MessageID)))
|
|
}
|