/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_statistics.h" #include "api/api_statistics_data_deserialize.h" #include "apiwrap.h" #include "base/unixtime.h" #include "data/data_channel.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_story.h" #include "history/history.h" #include "main/main_session.h" namespace Api { namespace { [[nodiscard]] Data::StatisticalValue StatisticalValueFromTL( const MTPStatsAbsValueAndPrev &tl) { const auto current = tl.data().vcurrent().v; const auto previous = tl.data().vprevious().v; return Data::StatisticalValue{ .value = current, .previousValue = previous, .growthRatePercentage = previous ? std::abs((current - previous) / float64(previous) * 100.) : 0, }; } [[nodiscard]] Data::ChannelStatistics ChannelStatisticsFromTL( const MTPDstats_broadcastStats &data) { const auto &tlUnmuted = data.venabled_notifications().data(); const auto unmuted = (!tlUnmuted.vtotal().v) ? 0. : std::clamp( tlUnmuted.vpart().v / tlUnmuted.vtotal().v * 100., 0., 100.); using Recent = MTPPostInteractionCounters; auto recentMessages = ranges::views::all( data.vrecent_posts_interactions().v ) | ranges::views::transform([&](const Recent &tl) { return tl.match([&](const MTPDpostInteractionCountersStory &data) { return Data::StatisticsMessageInteractionInfo{ .storyId = data.vstory_id().v, .viewsCount = data.vviews().v, .forwardsCount = data.vforwards().v, .reactionsCount = data.vreactions().v, }; }, [&](const MTPDpostInteractionCountersMessage &data) { return Data::StatisticsMessageInteractionInfo{ .messageId = data.vmsg_id().v, .viewsCount = data.vviews().v, .forwardsCount = data.vforwards().v, .reactionsCount = data.vreactions().v, }; }); }) | ranges::to_vector; return { .startDate = data.vperiod().data().vmin_date().v, .endDate = data.vperiod().data().vmax_date().v, .memberCount = StatisticalValueFromTL(data.vfollowers()), .meanViewCount = StatisticalValueFromTL(data.vviews_per_post()), .meanShareCount = StatisticalValueFromTL(data.vshares_per_post()), .meanReactionCount = StatisticalValueFromTL( data.vreactions_per_post()), .meanStoryViewCount = StatisticalValueFromTL( data.vviews_per_story()), .meanStoryShareCount = StatisticalValueFromTL( data.vshares_per_story()), .meanStoryReactionCount = StatisticalValueFromTL( data.vreactions_per_story()), .enabledNotificationsPercentage = unmuted, .memberCountGraph = StatisticalGraphFromTL( data.vgrowth_graph()), .joinGraph = StatisticalGraphFromTL( data.vfollowers_graph()), .muteGraph = StatisticalGraphFromTL( data.vmute_graph()), .viewCountByHourGraph = StatisticalGraphFromTL( data.vtop_hours_graph()), .viewCountBySourceGraph = StatisticalGraphFromTL( data.vviews_by_source_graph()), .joinBySourceGraph = StatisticalGraphFromTL( data.vnew_followers_by_source_graph()), .languageGraph = StatisticalGraphFromTL( data.vlanguages_graph()), .messageInteractionGraph = StatisticalGraphFromTL( data.vinteractions_graph()), .instantViewInteractionGraph = StatisticalGraphFromTL( data.viv_interactions_graph()), .reactionsByEmotionGraph = StatisticalGraphFromTL( data.vreactions_by_emotion_graph()), .storyInteractionsGraph = StatisticalGraphFromTL( data.vstory_interactions_graph()), .storyReactionsByEmotionGraph = StatisticalGraphFromTL( data.vstory_reactions_by_emotion_graph()), .recentMessageInteractions = std::move(recentMessages), }; } [[nodiscard]] Data::SupergroupStatistics SupergroupStatisticsFromTL( const MTPDstats_megagroupStats &data) { using Senders = MTPStatsGroupTopPoster; using Administrators = MTPStatsGroupTopAdmin; using Inviters = MTPStatsGroupTopInviter; auto topSenders = ranges::views::all( data.vtop_posters().v ) | ranges::views::transform([&](const Senders &tl) { return Data::StatisticsMessageSenderInfo{ .userId = UserId(tl.data().vuser_id().v), .sentMessageCount = tl.data().vmessages().v, .averageCharacterCount = tl.data().vavg_chars().v, }; }) | ranges::to_vector; auto topAdministrators = ranges::views::all( data.vtop_admins().v ) | ranges::views::transform([&](const Administrators &tl) { return Data::StatisticsAdministratorActionsInfo{ .userId = UserId(tl.data().vuser_id().v), .deletedMessageCount = tl.data().vdeleted().v, .bannedUserCount = tl.data().vkicked().v, .restrictedUserCount = tl.data().vbanned().v, }; }) | ranges::to_vector; auto topInviters = ranges::views::all( data.vtop_inviters().v ) | ranges::views::transform([&](const Inviters &tl) { return Data::StatisticsInviterInfo{ .userId = UserId(tl.data().vuser_id().v), .addedMemberCount = tl.data().vinvitations().v, }; }) | ranges::to_vector; return { .startDate = data.vperiod().data().vmin_date().v, .endDate = data.vperiod().data().vmax_date().v, .memberCount = StatisticalValueFromTL(data.vmembers()), .messageCount = StatisticalValueFromTL(data.vmessages()), .viewerCount = StatisticalValueFromTL(data.vviewers()), .senderCount = StatisticalValueFromTL(data.vposters()), .memberCountGraph = StatisticalGraphFromTL( data.vgrowth_graph()), .joinGraph = StatisticalGraphFromTL( data.vmembers_graph()), .joinBySourceGraph = StatisticalGraphFromTL( data.vnew_members_by_source_graph()), .languageGraph = StatisticalGraphFromTL( data.vlanguages_graph()), .messageContentGraph = StatisticalGraphFromTL( data.vmessages_graph()), .actionGraph = StatisticalGraphFromTL( data.vactions_graph()), .dayGraph = StatisticalGraphFromTL( data.vtop_hours_graph()), .weekGraph = StatisticalGraphFromTL( data.vweekdays_graph()), .topSenders = std::move(topSenders), .topAdministrators = std::move(topAdministrators), .topInviters = std::move(topInviters), }; } } // namespace Statistics::Statistics(not_null channel) : StatisticsRequestSender(channel) { } rpl::producer Statistics::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); if (!channel()->isMegagroup()) { makeRequest(MTPstats_GetBroadcastStats( MTP_flags(MTPstats_GetBroadcastStats::Flags(0)), channel()->inputChannel )).done([=](const MTPstats_BroadcastStats &result) { _channelStats = ChannelStatisticsFromTL(result.data()); consumer.put_done(); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); } else { makeRequest(MTPstats_GetMegagroupStats( MTP_flags(MTPstats_GetMegagroupStats::Flags(0)), channel()->inputChannel )).done([=](const MTPstats_MegagroupStats &result) { const auto &data = result.data(); _supergroupStats = SupergroupStatisticsFromTL(data); channel()->owner().processUsers(data.vusers()); consumer.put_done(); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); } return lifetime; }; } Statistics::GraphResult Statistics::requestZoom( const QString &token, float64 x) { return [=](auto consumer) { auto lifetime = rpl::lifetime(); const auto wasEmpty = _zoomDeque.empty(); _zoomDeque.push_back([=] { makeRequest(MTPstats_LoadAsyncGraph( MTP_flags(x ? MTPstats_LoadAsyncGraph::Flag::f_x : MTPstats_LoadAsyncGraph::Flag(0)), MTP_string(token), MTP_long(x) )).done([=](const MTPStatsGraph &result) { consumer.put_next(StatisticalGraphFromTL(result)); consumer.put_done(); if (!_zoomDeque.empty()) { _zoomDeque.pop_front(); if (!_zoomDeque.empty()) { _zoomDeque.front()(); } } }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); }); if (wasEmpty) { _zoomDeque.front()(); } return lifetime; }; } Data::ChannelStatistics Statistics::channelStats() const { return _channelStats; } Data::SupergroupStatistics Statistics::supergroupStats() const { return _supergroupStats; } PublicForwards::PublicForwards( not_null channel, Data::RecentPostId fullId) : StatisticsRequestSender(channel) , _fullId(fullId) { } void PublicForwards::request( const Data::PublicForwardsSlice::OffsetToken &token, Fn done) { if (_requestId) { return; } const auto channel = StatisticsRequestSender::channel(); const auto processResult = [=](const MTPstats_PublicForwards &tl) { using Messages = QVector; _requestId = 0; const auto &data = tl.data(); auto &owner = channel->owner(); owner.processUsers(data.vusers()); owner.processChats(data.vchats()); const auto nextToken = data.vnext_offset() ? qs(*data.vnext_offset()) : Data::PublicForwardsSlice::OffsetToken(); const auto fullCount = data.vcount().v; auto recentList = Messages(data.vforwards().v.size()); for (const auto &tlForward : data.vforwards().v) { tlForward.match([&](const MTPDpublicForwardMessage &data) { const auto &message = data.vmessage(); const auto msgId = IdFromMessage(message); const auto peerId = PeerFromMessage(message); const auto lastDate = DateFromMessage(message); if (const auto peer = owner.peerLoaded(peerId)) { if (!lastDate) { return; } owner.addNewMessage( message, MessageFlags(), NewMessageType::Existing); recentList.push_back({ .messageId = { peerId, msgId } }); } }, [&](const MTPDpublicForwardStory &data) { const auto story = owner.stories().applySingle( peerFromMTP(data.vpeer()), data.vstory()); if (story) { recentList.push_back({ .storyId = story->fullId() }); } }); } const auto allLoaded = nextToken.isEmpty() || (nextToken == token); _lastTotal = std::max(_lastTotal, fullCount); done({ .list = std::move(recentList), .total = _lastTotal, .allLoaded = allLoaded, .token = nextToken, }); }; constexpr auto kLimit = tl::make_int(100); if (_fullId.messageId) { _requestId = makeRequest(MTPstats_GetMessagePublicForwards( channel->inputChannel, MTP_int(_fullId.messageId.msg), MTP_string(token), kLimit )).done(processResult).fail([=] { _requestId = 0; }).send(); } else if (_fullId.storyId) { _requestId = makeRequest(MTPstats_GetStoryPublicForwards( channel->input, MTP_int(_fullId.storyId.story), MTP_string(token), kLimit )).done(processResult).fail([=] { _requestId = 0; }).send(); } } MessageStatistics::MessageStatistics( not_null channel, FullMsgId fullId) : StatisticsRequestSender(channel) , _publicForwards(channel, { .messageId = fullId }) , _fullId(fullId) { } MessageStatistics::MessageStatistics( not_null channel, FullStoryId storyId) : StatisticsRequestSender(channel) , _publicForwards(channel, { .storyId = storyId }) , _storyId(storyId) { } Data::PublicForwardsSlice MessageStatistics::firstSlice() const { return _firstSlice; } void MessageStatistics::request(Fn done) { if (channel()->isMegagroup()) { return; } const auto requestFirstPublicForwards = [=]( const Data::StatisticalGraph &messageGraph, const Data::StatisticalGraph &reactionsGraph, const Data::StatisticsMessageInteractionInfo &info) { const auto callback = [=](Data::PublicForwardsSlice slice) { const auto total = slice.total; _firstSlice = std::move(slice); done({ .messageInteractionGraph = messageGraph, .reactionsByEmotionGraph = reactionsGraph, .publicForwards = total, .privateForwards = info.forwardsCount - total, .views = info.viewsCount, .reactions = info.reactionsCount, }); }; _publicForwards.request({}, callback); }; const auto requestPrivateForwards = [=]( const Data::StatisticalGraph &messageGraph, const Data::StatisticalGraph &reactionsGraph) { api().request(MTPchannels_GetMessages( channel()->inputChannel, MTP_vector( 1, MTP_inputMessageID(MTP_int(_fullId.msg)))) ).done([=](const MTPmessages_Messages &result) { const auto process = [&](const MTPVector &messages) { const auto &message = messages.v.front(); return message.match([&](const MTPDmessage &data) { auto reactionsCount = 0; if (const auto tlReactions = data.vreactions()) { const auto &tlCounts = tlReactions->data().vresults(); for (const auto &tlCount : tlCounts.v) { reactionsCount += tlCount.data().vcount().v; } } return Data::StatisticsMessageInteractionInfo{ .messageId = IdFromMessage(message), .viewsCount = data.vviews() ? data.vviews()->v : 0, .forwardsCount = data.vforwards() ? data.vforwards()->v : 0, .reactionsCount = reactionsCount, }; }, [](const MTPDmessageEmpty &) { return Data::StatisticsMessageInteractionInfo(); }, [](const MTPDmessageService &) { return Data::StatisticsMessageInteractionInfo(); }); }; auto info = result.match([&](const MTPDmessages_messages &data) { return process(data.vmessages()); }, [&](const MTPDmessages_messagesSlice &data) { return process(data.vmessages()); }, [&](const MTPDmessages_channelMessages &data) { return process(data.vmessages()); }, [](const MTPDmessages_messagesNotModified &) { return Data::StatisticsMessageInteractionInfo(); }); requestFirstPublicForwards( messageGraph, reactionsGraph, std::move(info)); }).fail([=](const MTP::Error &error) { requestFirstPublicForwards(messageGraph, reactionsGraph, {}); }).send(); }; const auto requestStoryPrivateForwards = [=]( const Data::StatisticalGraph &messageGraph, const Data::StatisticalGraph &reactionsGraph) { api().request(MTPstories_GetStoriesByID( channel()->input, MTP_vector(1, MTP_int(_storyId.story))) ).done([=](const MTPstories_Stories &result) { const auto &storyItem = result.data().vstories().v.front(); auto info = storyItem.match([&](const MTPDstoryItem &data) { if (!data.vviews()) { return Data::StatisticsMessageInteractionInfo(); } const auto &tlViews = data.vviews()->data(); return Data::StatisticsMessageInteractionInfo{ .storyId = data.vid().v, .viewsCount = tlViews.vviews_count().v, .forwardsCount = tlViews.vforwards_count().value_or(0), .reactionsCount = tlViews.vreactions_count().value_or(0), }; }, [](const auto &) { return Data::StatisticsMessageInteractionInfo(); }); requestFirstPublicForwards( messageGraph, reactionsGraph, std::move(info)); }).fail([=](const MTP::Error &error) { requestFirstPublicForwards(messageGraph, reactionsGraph, {}); }).send(); }; if (_storyId) { makeRequest(MTPstats_GetStoryStats( MTP_flags(MTPstats_GetStoryStats::Flags(0)), channel()->input, MTP_int(_storyId.story) )).done([=](const MTPstats_StoryStats &result) { const auto &data = result.data(); requestStoryPrivateForwards( StatisticalGraphFromTL(data.vviews_graph()), StatisticalGraphFromTL(data.vreactions_by_emotion_graph())); }).fail([=](const MTP::Error &error) { requestStoryPrivateForwards({}, {}); }).send(); } else { makeRequest(MTPstats_GetMessageStats( MTP_flags(MTPstats_GetMessageStats::Flags(0)), channel()->inputChannel, MTP_int(_fullId.msg.bare) )).done([=](const MTPstats_MessageStats &result) { const auto &data = result.data(); requestPrivateForwards( StatisticalGraphFromTL(data.vviews_graph()), StatisticalGraphFromTL(data.vreactions_by_emotion_graph())); }).fail([=](const MTP::Error &error) { requestPrivateForwards({}, {}); }).send(); } } Boosts::Boosts(not_null peer) : _peer(peer) , _api(&peer->session().api().instance()) { } rpl::producer Boosts::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); const auto channel = _peer->asChannel(); if (!channel) { return lifetime; } _api.request(MTPpremium_GetBoostsStatus( _peer->input )).done([=](const MTPpremium_BoostsStatus &result) { const auto &data = result.data(); channel->updateLevelHint(data.vlevel().v); const auto hasPremium = !!data.vpremium_audience(); const auto premiumMemberCount = hasPremium ? std::max(0, int(data.vpremium_audience()->data().vpart().v)) : 0; const auto participantCount = hasPremium ? std::max( int(data.vpremium_audience()->data().vtotal().v), premiumMemberCount) : 0; const auto premiumMemberPercentage = (participantCount > 0) ? (100. * premiumMemberCount / participantCount) : 0; const auto slots = data.vmy_boost_slots(); _boostStatus.overview = Data::BoostsOverview{ .group = channel->isMegagroup(), .mine = slots ? int(slots->v.size()) : 0, .level = std::max(data.vlevel().v, 0), .boostCount = std::max( data.vboosts().v, data.vcurrent_level_boosts().v), .currentLevelBoostCount = data.vcurrent_level_boosts().v, .nextLevelBoostCount = data.vnext_level_boosts() ? data.vnext_level_boosts()->v : 0, .premiumMemberCount = premiumMemberCount, .premiumMemberPercentage = premiumMemberPercentage, }; _boostStatus.link = qs(data.vboost_url()); if (data.vprepaid_giveaways()) { _boostStatus.prepaidGiveaway = ranges::views::all( data.vprepaid_giveaways()->v ) | ranges::views::transform([](const MTPPrepaidGiveaway &r) { return Data::BoostPrepaidGiveaway{ .months = r.data().vmonths().v, .id = r.data().vid().v, .quantity = r.data().vquantity().v, .date = QDateTime::fromSecsSinceEpoch( r.data().vdate().v), }; }) | ranges::to_vector; } using namespace Data; requestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) { _boostStatus.firstSliceBoosts = std::move(slice); requestBoosts({ .gifts = true }, [=](BoostsListSlice &&s) { _boostStatus.firstSliceGifts = std::move(s); consumer.put_done(); }); }); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); return lifetime; }; } void Boosts::requestBoosts( const Data::BoostsListSlice::OffsetToken &token, Fn done) { if (_requestId) { return; } constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); constexpr auto kTlLimit = tl::make_int(kLimit); const auto gifts = token.gifts; _requestId = _api.request(MTPpremium_GetBoostsList( gifts ? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts) : MTP_flags(0), _peer->input, MTP_string(token.next), token.next.isEmpty() ? kTlFirstSlice : kTlLimit )).done([=](const MTPpremium_BoostsList &result) { _requestId = 0; const auto &data = result.data(); _peer->owner().processUsers(data.vusers()); auto list = std::vector(); list.reserve(data.vboosts().v.size()); constexpr auto kMonthsDivider = int(30 * 86400); for (const auto &boost : data.vboosts().v) { const auto &data = boost.data(); const auto path = data.vused_gift_slug() ? (u"giftcode/"_q + qs(data.vused_gift_slug()->v)) : QString(); auto giftCodeLink = !path.isEmpty() ? Data::GiftCodeLink{ _peer->session().createInternalLink(path), _peer->session().createInternalLinkFull(path), qs(data.vused_gift_slug()->v), } : Data::GiftCodeLink(); list.push_back({ data.is_gift(), data.is_giveaway(), data.is_unclaimed(), qs(data.vid()), data.vuser_id().value_or_empty(), data.vgiveaway_msg_id() ? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v } : FullMsgId(), QDateTime::fromSecsSinceEpoch(data.vdate().v), QDateTime::fromSecsSinceEpoch(data.vexpires().v), (data.vexpires().v - data.vdate().v) / kMonthsDivider, std::move(giftCodeLink), data.vmultiplier().value_or_empty(), }); } done(Data::BoostsListSlice{ .list = std::move(list), .multipliedTotal = data.vcount().v, .allLoaded = (data.vcount().v == data.vboosts().v.size()), .token = Data::BoostsListSlice::OffsetToken{ .next = data.vnext_offset() ? qs(*data.vnext_offset()) : QString(), .gifts = gifts, }, }); }).fail([=] { _requestId = 0; }).send(); } Data::BoostStatus Boosts::boostStatus() const { return _boostStatus; } ChannelEarnStatistics::ChannelEarnStatistics(not_null channel) : StatisticsRequestSender(channel) { } rpl::producer ChannelEarnStatistics::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); makeRequest(MTPstats_GetBroadcastRevenueStats( MTP_flags(0), channel()->inputChannel )).done([=](const MTPstats_BroadcastRevenueStats &result) { const auto &data = result.data(); const auto &balances = data.vbalances().data(); _data = Data::EarnStatistics{ .topHoursGraph = StatisticalGraphFromTL( data.vtop_hours_graph()), .revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()), .currentBalance = balances.vcurrent_balance().v, .availableBalance = balances.vavailable_balance().v, .overallRevenue = balances.voverall_revenue().v, .usdRate = data.vusd_rate().v, }; requestHistory({}, [=](Data::EarnHistorySlice &&slice) { _data.firstHistorySlice = std::move(slice); api().request( MTPchannels_GetFullChannel(channel()->inputChannel) ).done([=](const MTPmessages_ChatFull &result) { result.data().vfull_chat().match([&]( const MTPDchannelFull &d) { _data.switchedOff = d.is_restricted_sponsored(); }, [](const auto &) { }); consumer.put_done(); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); }); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); }).send(); return lifetime; }; } void ChannelEarnStatistics::requestHistory( const Data::EarnHistorySlice::OffsetToken &token, Fn done) { if (_requestId) { return; } constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); constexpr auto kTlLimit = tl::make_int(kLimit); _requestId = api().request(MTPstats_GetBroadcastRevenueTransactions( channel()->inputChannel, MTP_int(token), (!token) ? kTlFirstSlice : kTlLimit )).done([=](const MTPstats_BroadcastRevenueTransactions &result) { _requestId = 0; const auto &tlTransactions = result.data().vtransactions().v; auto list = std::vector(); list.reserve(tlTransactions.size()); for (const auto &tlTransaction : tlTransactions) { list.push_back(tlTransaction.match([&]( const MTPDbroadcastRevenueTransactionProceeds &d) { return Data::EarnHistoryEntry{ .type = Data::EarnHistoryEntry::Type::In, .amount = d.vamount().v, .date = base::unixtime::parse(d.vfrom_date().v), .dateTo = base::unixtime::parse(d.vto_date().v), }; }, [&](const MTPDbroadcastRevenueTransactionWithdrawal &d) { return Data::EarnHistoryEntry{ .type = Data::EarnHistoryEntry::Type::Out, .status = d.is_pending() ? Data::EarnHistoryEntry::Status::Pending : d.is_failed() ? Data::EarnHistoryEntry::Status::Failed : Data::EarnHistoryEntry::Status::Success, .amount = (std::numeric_limits::max() - d.vamount().v + 1), .date = base::unixtime::parse(d.vdate().v), // .provider = qs(d.vprovider()), .successDate = d.vtransaction_date() ? base::unixtime::parse(d.vtransaction_date()->v) : QDateTime(), .successLink = d.vtransaction_url() ? qs(*d.vtransaction_url()) : QString(), }; }, [&](const MTPDbroadcastRevenueTransactionRefund &d) { return Data::EarnHistoryEntry{ .type = Data::EarnHistoryEntry::Type::Return, .amount = d.vamount().v, .date = base::unixtime::parse(d.vdate().v), // .provider = qs(d.vprovider()), }; })); } const auto nextToken = token + tlTransactions.size(); done(Data::EarnHistorySlice{ .list = std::move(list), .total = result.data().vcount().v, .allLoaded = (result.data().vcount().v == nextToken), .token = Data::EarnHistorySlice::OffsetToken(nextToken), }); }).fail([=] { done({}); _requestId = 0; }).send(); } Data::EarnStatistics ChannelEarnStatistics::data() const { return _data; } } // namespace Api