2023-04-26 20:05:12 +00:00
|
|
|
/*
|
|
|
|
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 "apiwrap.h"
|
2023-04-26 20:58:03 +00:00
|
|
|
#include "data/data_channel.h"
|
2023-04-26 20:05:12 +00:00
|
|
|
#include "data/data_peer.h"
|
2023-10-09 02:09:23 +00:00
|
|
|
#include "data/data_session.h"
|
|
|
|
#include "history/history.h"
|
2023-04-26 20:05:12 +00:00
|
|
|
#include "main/main_session.h"
|
2023-05-02 14:33:17 +00:00
|
|
|
#include "statistics/statistics_data_deserialize.h"
|
2023-04-26 20:05:12 +00:00
|
|
|
|
|
|
|
namespace Api {
|
|
|
|
namespace {
|
2023-04-26 20:58:03 +00:00
|
|
|
|
2023-05-02 14:33:17 +00:00
|
|
|
[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL(
|
|
|
|
const MTPStatsGraph &tl) {
|
|
|
|
return tl.match([&](const MTPDstatsGraph &d) {
|
|
|
|
using namespace Statistic;
|
2023-07-27 04:03:53 +00:00
|
|
|
const auto zoomToken = d.vzoom_token().has_value()
|
|
|
|
? qs(*d.vzoom_token()).toUtf8()
|
|
|
|
: QByteArray();
|
2023-05-02 14:33:17 +00:00
|
|
|
return Data::StatisticalGraph{
|
|
|
|
StatisticalChartFromJSON(qs(d.vjson().data().vdata()).toUtf8()),
|
2023-07-27 04:03:53 +00:00
|
|
|
zoomToken,
|
2023-05-02 14:33:17 +00:00
|
|
|
};
|
|
|
|
}, [&](const MTPDstatsGraphAsync &data) {
|
2023-07-27 04:03:53 +00:00
|
|
|
return Data::StatisticalGraph{
|
|
|
|
.zoomToken = qs(data.vtoken()).toUtf8(),
|
|
|
|
};
|
2023-05-02 14:33:17 +00:00
|
|
|
}, [&](const MTPDstatsGraphError &data) {
|
|
|
|
return Data::StatisticalGraph{ Data::StatisticalChart() };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-26 20:58:03 +00:00
|
|
|
[[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 = MTPMessageInteractionCounters;
|
|
|
|
auto recentMessages = ranges::views::all(
|
|
|
|
data.vrecent_message_interactions().v
|
|
|
|
) | ranges::views::transform([&](const Recent &tl) {
|
|
|
|
return Data::StatisticsMessageInteractionInfo{
|
|
|
|
.messageId = tl.data().vmsg_id().v,
|
|
|
|
.viewsCount = tl.data().vviews().v,
|
|
|
|
.forwardsCount = tl.data().vforwards().v,
|
|
|
|
};
|
|
|
|
}) | ranges::to_vector;
|
2023-05-02 14:33:17 +00:00
|
|
|
|
2023-04-26 20:58:03 +00:00
|
|
|
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()),
|
|
|
|
|
|
|
|
.enabledNotificationsPercentage = unmuted,
|
|
|
|
|
2023-05-02 14:33:17 +00:00
|
|
|
.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()),
|
|
|
|
|
2023-04-26 20:58:03 +00:00
|
|
|
.recentMessageInteractions = std::move(recentMessages),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-09-28 20:20:07 +00:00
|
|
|
[[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),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-26 20:05:12 +00:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
Statistics::Statistics(not_null<ApiWrap*> api)
|
|
|
|
: _api(&api->instance()) {
|
|
|
|
}
|
|
|
|
|
2023-04-26 20:58:03 +00:00
|
|
|
rpl::producer<rpl::no_value, QString> Statistics::request(
|
|
|
|
not_null<PeerData*> peer) {
|
|
|
|
return [=](auto consumer) {
|
|
|
|
auto lifetime = rpl::lifetime();
|
|
|
|
const auto channel = peer->asChannel();
|
|
|
|
if (!channel) {
|
|
|
|
return lifetime;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!channel->isMegagroup()) {
|
|
|
|
_api.request(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();
|
2023-09-28 20:20:07 +00:00
|
|
|
} else {
|
|
|
|
_api.request(MTPstats_GetMegagroupStats(
|
|
|
|
MTP_flags(MTPstats_GetMegagroupStats::Flags(0)),
|
|
|
|
channel->inputChannel
|
|
|
|
)).done([=](const MTPstats_MegagroupStats &result) {
|
|
|
|
_supergroupStats = SupergroupStatisticsFromTL(result.data());
|
|
|
|
peer->owner().processUsers(result.data().vusers());
|
|
|
|
consumer.put_done();
|
|
|
|
}).fail([=](const MTP::Error &error) {
|
|
|
|
consumer.put_error_copy(error.type());
|
|
|
|
}).send();
|
2023-04-26 20:58:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return lifetime;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-09-28 22:47:51 +00:00
|
|
|
Statistics::GraphResult Statistics::requestZoom(
|
2023-07-27 04:03:53 +00:00
|
|
|
not_null<PeerData*> peer,
|
|
|
|
const QString &token,
|
|
|
|
float64 x) {
|
|
|
|
return [=](auto consumer) {
|
|
|
|
auto lifetime = rpl::lifetime();
|
|
|
|
const auto channel = peer->asChannel();
|
|
|
|
if (!channel) {
|
|
|
|
return lifetime;
|
|
|
|
}
|
|
|
|
|
|
|
|
_api.request(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();
|
|
|
|
}).fail([=](const MTP::Error &error) {
|
|
|
|
consumer.put_error_copy(error.type());
|
|
|
|
}).send();
|
|
|
|
|
|
|
|
return lifetime;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-09-28 22:47:51 +00:00
|
|
|
Statistics::GraphResult Statistics::requestMessage(
|
|
|
|
not_null<PeerData*> peer,
|
|
|
|
MsgId msgId) {
|
|
|
|
return [=](auto consumer) {
|
|
|
|
auto lifetime = rpl::lifetime();
|
|
|
|
const auto channel = peer->asChannel();
|
|
|
|
if (!channel) {
|
|
|
|
return lifetime;
|
|
|
|
}
|
|
|
|
|
|
|
|
_api.request(MTPstats_GetMessageStats(
|
|
|
|
MTP_flags(MTPstats_GetMessageStats::Flags(0)),
|
|
|
|
channel->inputChannel,
|
|
|
|
MTP_int(msgId.bare)
|
|
|
|
)).done([=](const MTPstats_MessageStats &result) {
|
|
|
|
consumer.put_next(
|
|
|
|
StatisticalGraphFromTL(result.data().vviews_graph()));
|
|
|
|
consumer.put_done();
|
|
|
|
}).fail([=](const MTP::Error &error) {
|
|
|
|
consumer.put_error_copy(error.type());
|
|
|
|
}).send();
|
|
|
|
|
|
|
|
return lifetime;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-26 20:58:03 +00:00
|
|
|
Data::ChannelStatistics Statistics::channelStats() const {
|
|
|
|
return _channelStats;
|
2023-04-26 20:05:12 +00:00
|
|
|
}
|
|
|
|
|
2023-09-28 20:20:07 +00:00
|
|
|
Data::SupergroupStatistics Statistics::supergroupStats() const {
|
|
|
|
return _supergroupStats;
|
|
|
|
}
|
|
|
|
|
2023-10-09 02:09:23 +00:00
|
|
|
PublicForwards::PublicForwards(
|
|
|
|
not_null<ChannelData*> channel,
|
|
|
|
FullMsgId fullId)
|
|
|
|
: _channel(channel)
|
|
|
|
, _fullId(fullId)
|
|
|
|
, _api(&channel->session().api().instance()) {
|
|
|
|
}
|
|
|
|
|
|
|
|
void PublicForwards::request(
|
|
|
|
const OffsetToken &token,
|
|
|
|
Fn<void(Slice)> done) {
|
|
|
|
if (_requestId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto offsetPeer = _channel->owner().peer(token.fullId.peer);
|
|
|
|
const auto tlOffsetPeer = offsetPeer
|
|
|
|
? offsetPeer->input
|
|
|
|
: MTP_inputPeerEmpty();
|
|
|
|
constexpr auto kLimit = tl::make_int(100);
|
|
|
|
_requestId = _api.request(MTPstats_GetMessagePublicForwards(
|
|
|
|
_channel->inputChannel,
|
|
|
|
MTP_int(_fullId.msg),
|
|
|
|
MTP_int(token.rate),
|
|
|
|
tlOffsetPeer,
|
|
|
|
MTP_int(token.fullId.msg),
|
|
|
|
kLimit
|
|
|
|
)).done([=, channel = _channel](const MTPmessages_Messages &result) {
|
|
|
|
using Messages = QVector<FullMsgId>;
|
|
|
|
_requestId = 0;
|
|
|
|
|
|
|
|
auto nextToken = OffsetToken();
|
|
|
|
const auto process = [&](const MTPVector<MTPMessage> &messages) {
|
|
|
|
auto result = Messages();
|
|
|
|
for (const auto &message : messages.v) {
|
|
|
|
const auto msgId = IdFromMessage(message);
|
|
|
|
const auto peerId = PeerFromMessage(message);
|
|
|
|
const auto lastDate = DateFromMessage(message);
|
|
|
|
if (const auto peer = channel->owner().peerLoaded(peerId)) {
|
|
|
|
if (lastDate) {
|
|
|
|
channel->owner().addNewMessage(
|
|
|
|
message,
|
|
|
|
MessageFlags(),
|
|
|
|
NewMessageType::Existing);
|
|
|
|
nextToken.fullId = { peerId, msgId };
|
|
|
|
result.push_back(nextToken.fullId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto allLoaded = false;
|
|
|
|
auto fullCount = 0;
|
|
|
|
auto messages = result.match([&](const MTPDmessages_messages &data) {
|
|
|
|
channel->owner().processUsers(data.vusers());
|
|
|
|
channel->owner().processChats(data.vchats());
|
|
|
|
auto list = process(data.vmessages());
|
|
|
|
allLoaded = true;
|
|
|
|
fullCount = list.size();
|
|
|
|
return list;
|
|
|
|
}, [&](const MTPDmessages_messagesSlice &data) {
|
|
|
|
channel->owner().processUsers(data.vusers());
|
|
|
|
channel->owner().processChats(data.vchats());
|
|
|
|
auto list = process(data.vmessages());
|
|
|
|
|
|
|
|
if (const auto nextRate = data.vnext_rate()) {
|
|
|
|
const auto rateUpdated = (nextRate->v != token.rate);
|
|
|
|
if (rateUpdated) {
|
|
|
|
nextToken.rate = nextRate->v;
|
|
|
|
} else {
|
|
|
|
allLoaded = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fullCount = data.vcount().v;
|
|
|
|
return list;
|
|
|
|
}, [&](const MTPDmessages_channelMessages &data) {
|
|
|
|
channel->owner().processUsers(data.vusers());
|
|
|
|
channel->owner().processChats(data.vchats());
|
|
|
|
auto list = process(data.vmessages());
|
|
|
|
allLoaded = true;
|
|
|
|
fullCount = data.vcount().v;
|
|
|
|
return list;
|
|
|
|
}, [&](const MTPDmessages_messagesNotModified &) {
|
|
|
|
allLoaded = true;
|
|
|
|
return Messages();
|
|
|
|
});
|
|
|
|
|
|
|
|
_lastTotal = std::max(_lastTotal, fullCount);
|
|
|
|
done({
|
|
|
|
.list = std::move(messages),
|
|
|
|
.total = _lastTotal,
|
|
|
|
.allLoaded = allLoaded,
|
|
|
|
.token = nextToken,
|
|
|
|
});
|
|
|
|
}).fail([=] {
|
|
|
|
_requestId = 0;
|
|
|
|
}).send();
|
|
|
|
}
|
|
|
|
|
2023-10-09 14:10:43 +00:00
|
|
|
MessageStatistics::MessageStatistics(
|
|
|
|
not_null<ChannelData*> channel,
|
|
|
|
FullMsgId fullId)
|
|
|
|
: _publicForwards(channel, fullId)
|
|
|
|
, _channel(channel)
|
|
|
|
, _fullId(fullId)
|
|
|
|
, _api(&channel->session().api().instance()) {
|
|
|
|
}
|
|
|
|
|
|
|
|
PublicForwards::Slice MessageStatistics::firstSlice() const {
|
|
|
|
return _firstSlice;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MessageStatistics::request(Fn<void(Data::MessageStatistics)> done) {
|
|
|
|
if (_channel->isMegagroup()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto requestFirstPublicForwards = [=](
|
|
|
|
const Data::StatisticalGraph &messageGraph,
|
|
|
|
const Data::StatisticsMessageInteractionInfo &info) {
|
|
|
|
_publicForwards.request({}, [=](PublicForwards::Slice slice) {
|
|
|
|
const auto total = slice.total;
|
|
|
|
_firstSlice = std::move(slice);
|
|
|
|
done({
|
|
|
|
.messageInteractionGraph = messageGraph,
|
|
|
|
.publicForwards = total,
|
|
|
|
.privateForwards = info.forwardsCount - total,
|
|
|
|
.views = info.viewsCount,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const auto requestPrivateForwards = [=](
|
|
|
|
const Data::StatisticalGraph &messageGraph) {
|
|
|
|
_api.request(MTPstats_GetBroadcastStats(
|
|
|
|
MTP_flags(MTPstats_GetBroadcastStats::Flags(0)),
|
|
|
|
_channel->inputChannel
|
|
|
|
)).done([=](const MTPstats_BroadcastStats &result) {
|
|
|
|
const auto channelStats = ChannelStatisticsFromTL(result.data());
|
|
|
|
auto info = Data::StatisticsMessageInteractionInfo();
|
|
|
|
for (const auto &r : channelStats.recentMessageInteractions) {
|
|
|
|
if (r.messageId == _fullId.msg) {
|
|
|
|
info = r;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
requestFirstPublicForwards(messageGraph, info);
|
|
|
|
}).fail([=](const MTP::Error &error) {
|
|
|
|
requestFirstPublicForwards(messageGraph, {});
|
|
|
|
}).send();
|
|
|
|
};
|
|
|
|
|
|
|
|
_api.request(MTPstats_GetMessageStats(
|
|
|
|
MTP_flags(MTPstats_GetMessageStats::Flags(0)),
|
|
|
|
_channel->inputChannel,
|
|
|
|
MTP_int(_fullId.msg.bare)
|
|
|
|
)).done([=](const MTPstats_MessageStats &result) {
|
|
|
|
requestPrivateForwards(
|
|
|
|
StatisticalGraphFromTL(result.data().vviews_graph()));
|
|
|
|
}).fail([=](const MTP::Error &error) {
|
|
|
|
requestPrivateForwards({});
|
|
|
|
}).send();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-04-26 20:05:12 +00:00
|
|
|
} // namespace Api
|