/* 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_polls.h" #include "api/api_common.h" #include "api/api_statistics_data_deserialize.h" #include "api/api_text_entities.h" #include "api/api_updates.h" #include "apiwrap.h" #include "base/call_delayed.h" #include "base/qt/qt_key_modifiers.h" #include "base/random.h" #include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" #include "data/data_poll.h" #include "data/data_session.h" #include "data/data_statistics_chart.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" // ShouldSendSilent #include "lang/lang_keys.h" #include "main/main_session.h" #include "styles/style_polls.h" #include "ui/toast/toast.h" #include "window/window_session_controller.h" namespace { constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000); const auto kSubscribersOnlyVoteErrorPatterns = std::array{ u"POLL_SUBSCRIBERS_ONLY"_q, u"POLL_MEMBER_RESTRICTED"_q, u"VOTE_SUBSCRIBERS_ONLY"_q, u"SUBSCRIBERS_ONLY"_q, u"SUBSCRIBER_REQUIRED"_q, u"SUBSCRIBER_ONLY"_q, }; const auto kSubscribersJoinedTooRecentlyVoteErrorPatterns = std::array{ u"POLL_SUBSCRIBERS_TOO_RECENT"_q, u"VOTE_SUBSCRIBERS_TOO_RECENT"_q, u"SUBSCRIBERS_TOO_RECENT"_q, u"SUBSCRIBER_TOO_RECENT"_q, u"JOINED_TOO_RECENTLY"_q, u"24_HOURS"_q, }; const auto kCountriesVoteErrorPatterns = std::array{ u"POLL_COUNTRIES_ISO2"_q, u"VOTE_COUNTRIES_ISO2"_q, u"COUNTRIES_ISO2"_q, u"COUNTRY_RESTRICTED"_q, u"COUNTRY_ISO2"_q, }; template [[nodiscard]] bool MatchesErrorPattern( const QString &type, const std::array &patterns) { for (const auto &pattern : patterns) { if (!pattern.isEmpty() && type.contains(pattern, Qt::CaseInsensitive)) { return true; } } return false; } [[nodiscard]] PollData::VoteRestriction ParseVoteRestrictionError( const QString &type) { if (MatchesErrorPattern( type, kSubscribersJoinedTooRecentlyVoteErrorPatterns)) { return PollData::VoteRestriction::SubscribersJoinedTooRecently; } else if (MatchesErrorPattern( type, kSubscribersOnlyVoteErrorPatterns)) { return PollData::VoteRestriction::SubscribersOnly; } else if (MatchesErrorPattern( type, kCountriesVoteErrorPatterns)) { return PollData::VoteRestriction::Countries; } return PollData::VoteRestriction::None; } void ShowVoteRestrictionToast( not_null peer, not_null poll, PollData::VoteRestriction restriction) { if (restriction == PollData::VoteRestriction::None) { return; } auto text = PollVoteRestrictionText(restriction, peer, poll); if (text.text.isEmpty()) { return; } if (const auto window = peer->session().tryResolveWindow(peer)) { window->showToast({ .text = std::move(text), .iconLottie = u"ban"_q, .iconLottieSize = st::pollToastIconSize, .duration = kVoteRestrictionToastDuration, }); } } #ifdef _DEBUG [[nodiscard]] Data::StatisticalGraph GenerateMockupPollStats( const PollData &poll) { auto chart = Data::StatisticalChart(); const auto colorKeys = std::array{ u"BLUE"_q, u"GREEN"_q, u"RED"_q, u"GOLDEN"_q, u"LIGHTBLUE"_q, u"LIGHTGREEN"_q, u"ORANGE"_q, u"INDIGO"_q, u"PURPLE"_q, u"CYAN"_q, }; constexpr auto kPoints = 14; constexpr auto kOneDay = float64(24 * 60 * 60 * 1000); constexpr auto kStart = float64(1704067200000); chart.x.reserve(kPoints); for (auto i = 0; i != kPoints; ++i) { chart.x.push_back(kStart + i * kOneDay); } chart.timeStep = kOneDay; auto lineId = 0; chart.lines.reserve(poll.answers.size()); for (const auto &answer : poll.answers) { auto line = Data::StatisticalChart::Line(); line.id = ++lineId; line.idString = u"answer_%1"_q.arg(line.id); line.name = answer.text.text.trimmed(); if (line.name.isEmpty()) { line.name = QString("#%1").arg(line.id); } line.colorKey = colorKeys[(line.id - 1) % int(colorKeys.size())]; line.y.reserve(kPoints); auto seed = int64(13 * line.id + 17); for (const auto byte : answer.option) { seed += uchar(byte); } const auto base = std::max(int64(answer.votes), int64(1)); for (auto i = 0; i != kPoints; ++i) { const auto wave = int64( ((i + line.id) % 5) * ((i + 2 * line.id) % 4)); const auto trend = int64((i * (line.id + 1)) / 3); const auto noise = int64((seed + i * 7 + line.id * 11) % 6); const auto value = std::max( base + wave + trend + noise - 2, int64(1)); line.y.push_back(value); line.maxValue = std::max(line.maxValue, value); line.minValue = std::min(line.minValue, value); } chart.lines.push_back(std::move(line)); } if (chart.lines.empty()) { auto line = Data::StatisticalChart::Line(); line.id = 1; line.idString = u"votes"_q; line.name = tr::lng_notification_reactions_poll_votes(tr::now); line.colorKey = u"BLUE"_q; line.y.reserve(kPoints); const auto base = std::max(int64(poll.totalVoters), int64(1)); for (auto i = 0; i != kPoints; ++i) { const auto value = std::max( base + i * 2 + ((i * 5) % 7), int64(1)); line.y.push_back(value); line.maxValue = std::max(line.maxValue, value); line.minValue = std::min(line.minValue, value); } chart.lines.push_back(std::move(line)); } chart.defaultZoomXIndex = { .min = std::max(0, kPoints - 8), .max = kPoints - 1, }; chart.measure(); if (chart.maxValue == chart.minValue) { if (chart.minValue) { chart.minValue = 0; } else { chart.maxValue = 1; } } return { .chart = std::move(chart), }; } #endif } // namespace namespace Api { Polls::Polls(not_null api) : _session(&api->session()) , _api(&api->instance()) { } void Polls::create( const PollData &data, const TextWithEntities &text, SendAction action, Fn done, Fn fail) { StripEphemeralReply(_session, action.replyTo); _session->api().sendAction(action); const auto history = action.history; const auto peer = history->peer; const auto topicRootId = action.replyTo.messageId ? action.replyTo.topicRootId : 0; const auto monoforumPeerId = action.replyTo.monoforumPeerId; auto sendFlags = MTPmessages_SendMedia::Flags(0); if (action.replyTo) { sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto clearCloudDraft = action.clearDraft; if (clearCloudDraft) { sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; history->clearLocalDraft(topicRootId, monoforumPeerId); history->clearCloudDraft(topicRootId, monoforumPeerId); history->startSavingCloudDraft(topicRootId, monoforumPeerId); } const auto silentPost = ShouldSendSilent(peer, action.options); const auto starsPaid = std::min( peer->starsPerMessageChecked(), action.options.starsApproved); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } if (action.options.scheduled) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; if (action.options.scheduleRepeatPeriod) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period; } } if (action.options.shortcutId) { sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; } if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } if (action.options.suggest) { sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; } if (starsPaid) { action.options.starsApproved -= starsPaid; sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; } auto sentEntities = Api::EntitiesToMTP( _session, text.entities, Api::ConvertOption::SkipLocal); if (!sentEntities.v.isEmpty()) { sendFlags |= MTPmessages_SendMedia::Flag::f_entities; } auto &histories = history->owner().histories(); const auto randomId = base::RandomValue(); histories.sendPreparedMessage( history, action.replyTo, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input(), Data::Histories::ReplyToPlaceholder(), PollDataToInputMedia(&data), MTP_string(text.text), MTP_long(randomId), MTPReplyMarkup(), sentEntities, MTP_int(action.options.scheduled), MTP_int(action.options.scheduleRepeatPeriod), (sendAs ? sendAs->input() : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), MTP_long(action.options.effectId), MTP_long(starsPaid), SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( topicRootId, monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } _session->changes().historyUpdated( history, (action.options.scheduled ? Data::HistoryUpdate::Flag::ScheduledSent : Data::HistoryUpdate::Flag::MessageSent)); done(); }, [=](const MTP::Error &error, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( topicRootId, monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } const auto expired = (error.code() == 400) && error.type().startsWith(u"FILE_REFERENCE_"_q); fail(expired); }); } void Polls::sendVotes( FullMsgId itemId, const std::vector &options) { if (_pollVotesRequestIds.contains(itemId)) { return; } const auto item = _session->data().message(itemId); const auto media = item ? item->media() : nullptr; const auto poll = media ? media->poll() : nullptr; if (!item) { return; } const auto peer = item->history()->peer; const auto showSending = poll && !options.empty(); const auto hideSending = [=] { if (showSending) { if (const auto item = _session->data().message(itemId)) { poll->sendingVotes.clear(); _session->data().requestItemRepaint(item); } } }; if (showSending) { poll->sendingVotes = options; _session->data().requestItemRepaint(item); } else if (poll && options.empty() && poll->voted()) { for (auto &answer : poll->answers) { answer.chosen = false; } ++poll->version; _session->data().notifyPollUpdateDelayed(poll); } auto prepared = QVector(); prepared.reserve(options.size()); ranges::transform( options, ranges::back_inserter(prepared), [](const QByteArray &option) { return MTP_bytes(option); }); const auto requestId = _api.request(MTPmessages_SendVote( peer->input(), MTP_int(item->id), MTP_vector(prepared) )).done([=](const MTPUpdates &result) { _pollVotesRequestIds.erase(itemId); hideSending(); if (poll) { if (poll->voteRestriction() != PollData::VoteRestriction::None) { poll->setVoteRestriction(PollData::VoteRestriction::None); _session->data().notifyPollUpdateDelayed(poll); } } _session->updates().applyUpdates(result); }).fail([=](const MTP::Error &error) { _pollVotesRequestIds.erase(itemId); hideSending(); if (poll) { const auto restriction = ParseVoteRestrictionError(error.type()); if (restriction != PollData::VoteRestriction::None) { poll->setVoteRestriction(restriction); _session->data().notifyPollUpdateDelayed(poll); if (const auto item = _session->data().message(itemId)) { _session->data().requestItemResize(item); } ShowVoteRestrictionToast(peer, poll, restriction); } } }).send(); _pollVotesRequestIds.emplace(itemId, requestId); } void Polls::addAnswer( FullMsgId itemId, const TextWithEntities &text, const PollMedia &media, Fn done, Fn fail) { if (_pollAddAnswerRequestIds.contains(itemId)) { return; } const auto item = _session->data().message(itemId); if (!item) { return; } const auto sentEntities = Api::EntitiesToMTP( _session, text.entities, Api::ConvertOption::SkipLocal); using Flag = MTPDinputPollAnswer::Flag; const auto flags = media ? Flag::f_media : Flag(); const auto answer = MTP_inputPollAnswer( MTP_flags(flags), MTP_textWithEntities( MTP_string(text.text), sentEntities), media ? PollMediaToMTP(media) : MTPInputMedia()); const auto requestId = _api.request(MTPmessages_AddPollAnswer( item->history()->peer->input(), MTP_int(item->id), answer )).done([=](const MTPUpdates &result) { _pollAddAnswerRequestIds.erase(itemId); _session->updates().applyUpdates(result); if (done) { done(); } }).fail([=](const MTP::Error &error) { _pollAddAnswerRequestIds.erase(itemId); if (fail) { fail(error.type()); } }).send(); _pollAddAnswerRequestIds.emplace(itemId, requestId); } void Polls::deleteAnswer(FullMsgId itemId, const QByteArray &option) { if (_pollVotesRequestIds.contains(itemId)) { return; } const auto item = _session->data().message(itemId); if (!item) { return; } const auto requestId = _api.request(MTPmessages_DeletePollAnswer( item->history()->peer->input(), MTP_int(item->id), MTP_bytes(option) )).done([=](const MTPUpdates &result) { _pollVotesRequestIds.erase(itemId); _session->updates().applyUpdates(result); }).fail([=] { _pollVotesRequestIds.erase(itemId); }).send(); _pollVotesRequestIds.emplace(itemId, requestId); } void Polls::close(not_null item) { const auto itemId = item->fullId(); if (_pollCloseRequestIds.contains(itemId)) { return; } const auto media = item ? item->media() : nullptr; const auto poll = media ? media->poll() : nullptr; if (!poll) { return; } const auto requestId = _api.request(MTPmessages_EditMessage( MTP_flags(MTPmessages_EditMessage::Flag::f_media), item->history()->peer->input(), MTP_int(item->id), MTPstring(), PollDataToInputMedia(poll, true), MTPReplyMarkup(), MTPVector(), MTP_int(0), // schedule_date MTP_int(0), // schedule_repeat_period MTPint(), // quick_reply_shortcut_id MTPInputRichMessage() )).done([=](const MTPUpdates &result) { _pollCloseRequestIds.erase(itemId); _session->updates().applyUpdates(result); }).fail([=] { _pollCloseRequestIds.erase(itemId); }).send(); _pollCloseRequestIds.emplace(itemId, requestId); } void Polls::reloadResults(not_null item) { const auto itemId = item->fullId(); if (!item->isRegular() || _pollReloadRequestIds.contains(itemId)) { return; } const auto media = item->media(); const auto poll = media ? media->poll() : nullptr; const auto pollHash = poll ? poll->hash : uint64(0); const auto requestId = _api.request(MTPmessages_GetPollResults( item->history()->peer->input(), MTP_int(item->id), MTP_long(pollHash) )).done([=](const MTPUpdates &result) { _pollReloadRequestIds.erase(itemId); _session->updates().applyUpdates(result); }).fail([=] { _pollReloadRequestIds.erase(itemId); }).send(); _pollReloadRequestIds.emplace(itemId, requestId); } void Polls::requestStats( FullMsgId itemId, Fn done, Fn fail) { const auto item = _session->data().message(itemId); const auto media = item ? item->media() : nullptr; const auto poll = media ? media->poll() : nullptr; if (!item || !item->isRegular() || !poll) { if (fail) { fail(QString()); } return; } #ifdef _DEBUG if (base::IsCtrlPressed()) { auto callback = std::move(done); if (callback) { constexpr auto kMockupStatsDelay = 2 * crl::time(1000); auto graph = GenerateMockupPollStats(*poll); base::call_delayed(kMockupStatsDelay, _session, [=]() mutable { callback(std::move(graph)); }); } return; } #endif const auto requestGraph = [=](const QString &token) { _api.request(MTPstats_LoadAsyncGraph( MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)), MTP_string(token), MTP_long(0) )).done([=](const MTPStatsGraph &result) { if (done) { done(Api::StatisticalGraphFromTL(result)); } }).fail([=](const MTP::Error &error) { if (fail) { fail(error.type()); } }).send(); }; _api.request(MTPstats_GetPollStats( MTP_flags(MTPstats_GetPollStats::Flags(0)), item->history()->peer->input(), MTP_int(item->id) )).done([=](const MTPstats_PollStats &result) { auto graph = Api::StatisticalGraphFromTL(result.data().vvotes_graph()); if (graph.chart || !graph.error.isEmpty() || graph.zoomToken.isEmpty()) { if (done) { done(std::move(graph)); } } else { requestGraph(graph.zoomToken); } }).fail([=](const MTP::Error &error) { if (fail) { fail(error.type()); } }).send(); } } // namespace Api