252 lines
8.2 KiB
TypeScript

import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiChannelStatistics,
ApiGroupStatistics,
ApiMessagePublicForward,
ApiPostStatistics,
ApiStoryPublicForward,
PrepaidGiveaway, StatisticsGraph,
StatisticsMessageInteractionCounter,
StatisticsOverviewItem,
StatisticsOverviewPercentage,
StatisticsOverviewPeriod,
StatisticsStoryInteractionCounter,
} from '../../types';
import { buildApiUsernames, buildAvatarPhotoId } from './common';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
return {
// Graphs
growthGraph: buildGraph(stats.growthGraph),
followersGraph: buildGraph(stats.followersGraph),
muteGraph: buildGraph(stats.muteGraph),
topHoursGraph: buildGraph(stats.topHoursGraph),
// Async graphs
languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token,
viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token,
newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token,
interactionsGraph: (stats.interactionsGraph as GramJs.StatsGraphAsync).token,
reactionsByEmotionGraph: (stats.reactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
storyInteractionsGraph: (stats.storyInteractionsGraph as GramJs.StatsGraphAsync).token,
storyReactionsByEmotionGraph: (stats.storyReactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
// Statistics overview
followers: buildStatisticsOverview(stats.followers),
viewsPerPost: buildStatisticsOverview(stats.viewsPerPost),
sharesPerPost: buildStatisticsOverview(stats.sharesPerPost),
enabledNotifications: buildStatisticsPercentage(stats.enabledNotifications),
reactionsPerPost: buildStatisticsOverview(stats.reactionsPerPost),
viewsPerStory: buildStatisticsOverview(stats.viewsPerStory),
sharesPerStory: buildStatisticsOverview(stats.sharesPerStory),
reactionsPerStory: buildStatisticsOverview(stats.reactionsPerStory),
// Recent posts
recentPosts: stats.recentPostsInteractions.map(buildApiPostInteractionCounter).filter(Boolean),
};
}
export function buildApiPostInteractionCounter(
interaction: GramJs.TypePostInteractionCounters,
): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined {
if (interaction instanceof GramJs.PostInteractionCountersMessage) {
return {
msgId: interaction.msgId,
forwardsCount: interaction.forwards,
viewsCount: interaction.views,
reactionsCount: interaction.reactions,
};
}
if (interaction instanceof GramJs.PostInteractionCountersStory) {
return {
storyId: interaction.storyId,
reactionsCount: interaction.reactions,
viewsCount: interaction.views,
forwardsCount: interaction.forwards,
};
}
return undefined;
}
export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGroupStatistics {
return {
// Graphs
growthGraph: buildGraph(stats.growthGraph),
membersGraph: buildGraph(stats.membersGraph),
topHoursGraph: buildGraph(stats.topHoursGraph),
// Async graphs
languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token,
messagesGraph: (stats.messagesGraph as GramJs.StatsGraphAsync).token,
actionsGraph: (stats.actionsGraph as GramJs.StatsGraphAsync).token,
// Statistics overview
period: getOverviewPeriod(stats.period),
members: buildStatisticsOverview(stats.members),
viewers: buildStatisticsOverview(stats.viewers),
messages: buildStatisticsOverview(stats.messages),
posters: buildStatisticsOverview(stats.posters),
};
}
export function buildPostsStatistics(stats: GramJs.stats.MessageStats): ApiPostStatistics {
return {
viewsGraph: buildGraph(stats.viewsGraph),
reactionsGraph: buildGraph(stats.reactionsByEmotionGraph),
};
}
export function buildMessagePublicForwards(
result: GramJs.messages.TypeMessages,
): ApiMessagePublicForward[] | undefined {
if (!result || !('messages' in result)) {
return undefined;
}
return result.messages.map((message) => buildApiMessagePublicForward(message, result.chats));
}
export function buildStoryPublicForwards(
result: GramJs.stats.PublicForwards,
): Array<ApiStoryPublicForward | ApiMessagePublicForward> | undefined {
if (!result || !('forwards' in result)) {
return undefined;
}
return result.forwards.map((forward) => {
if (forward instanceof GramJs.PublicForwardMessage) {
return buildApiMessagePublicForward(forward.message, result.chats);
}
const { peer, story } = forward;
const peerId = getApiChatIdFromMtpPeer(peer);
return {
peerId,
storyId: story.id,
viewsCount: (story as GramJs.StoryItem).views?.viewsCount || 0,
reactionsCount: (story as GramJs.StoryItem).views?.reactionsCount || 0,
} as ApiStoryPublicForward;
});
}
export function buildGraph(
result: GramJs.TypeStatsGraph, isPercentage?: boolean,
): StatisticsGraph | undefined {
if ((result as GramJs.StatsGraphError).error) {
return undefined;
}
const data = JSON.parse((result as GramJs.StatsGraph).json.data);
const [x, ...y] = data.columns;
const hasSecondYAxis = data.y_scaled;
return {
type: isPercentage ? 'area' : data.types.y0,
zoomToken: (result as GramJs.StatsGraph).zoomToken,
labelFormatter: data.xTickFormatter,
tooltipFormatter: data.xTooltipFormatter,
labels: x.slice(1),
hideCaption: !data.subchart.show,
hasSecondYAxis,
isStacked: data.stacked && !hasSecondYAxis,
isPercentage,
datasets: y.map((item: any) => {
const key = item[0];
return {
name: data.names[key],
color: extractColor(data.colors[key]),
values: item.slice(1),
};
}),
...calculateMinimapRange(data.subchart.defaultZoom, x.slice(1)),
};
}
function extractColor(color: string): string {
return color.substring(color.indexOf('#'));
}
function calculateMinimapRange(range: Array<number>, values: Array<number>) {
const [min, max] = range;
let minIndex = 0;
let maxIndex = values.length - 1;
values.forEach((item, index) => {
if (!minIndex && item >= min) {
minIndex = index;
}
if (!maxIndex && item >= max) {
maxIndex = index;
}
});
const begin = Math.max(0, minIndex / (values.length - 1));
const end = Math.min(1, maxIndex / (values.length - 1));
return { minimapRange: { begin, end }, labelFromIndex: minIndex, labelToIndex: maxIndex };
}
function buildStatisticsOverview({ current, previous }: GramJs.StatsAbsValueAndPrev): StatisticsOverviewItem {
const change = current - previous;
return {
current,
change,
...(previous && { percentage: (change ? ((Math.abs(change) / previous) * 100) : 0).toFixed(2) }),
};
}
export function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOverviewPercentage {
return {
part: data.part,
total: data.total,
percentage: ((data.part / data.total) * 100).toFixed(2),
};
}
export function buildPrepaidGiveaway(prepaidGiveaway: GramJs.PrepaidGiveaway): PrepaidGiveaway {
return {
id: prepaidGiveaway.id.toString(),
date: prepaidGiveaway.date,
months: prepaidGiveaway.months,
quantity: prepaidGiveaway.quantity,
};
}
function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewPeriod {
return {
maxDate: data.maxDate,
minDate: data.minDate,
};
}
function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs.TypeChat[]): ApiMessagePublicForward {
const peerId = getApiChatIdFromMtpPeer(message.peerId!);
const channel = chats.find((c) => buildApiPeerId(c.id, 'channel') === peerId);
const channelProfilePhoto = channel && 'photo' in channel && channel.photo instanceof GramJs.Photo
? channel.photo : undefined;
return {
messageId: message.id,
views: (message as GramJs.Message).views,
title: (channel as GramJs.Channel).title,
chat: {
id: peerId,
type: 'chatTypeChannel',
title: (channel as GramJs.Channel).title,
usernames: buildApiUsernames(channel as GramJs.Channel),
avatarPhotoId: channelProfilePhoto && buildAvatarPhotoId(channelProfilePhoto),
hasVideoAvatar: Boolean(channelProfilePhoto?.videoSizes),
},
};
}