diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index cb8edb3d9..861e2ed1a 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -3427,10 +3427,16 @@ getJsonValue json:string = JsonValue; getJsonString json_value:JsonValue = Text; -//@description Changes the user answer to a poll. A quiz poll can be answered only once @chat_id Identifier of the chat to which the poll belongs @message_id Identifier of the message containing the poll -//@option_ids 0-based identifiers of options, chosen by the user. User can choose more than 1 option only is the poll allows multiple answers +//@description Changes the user answer to a poll. A quiz poll can be answered only once +//@chat_id Identifier of the chat to which the poll belongs @message_id Identifier of the message containing the poll +//@option_ids 0-based identifiers of answer options, chosen by the user. User can choose more than 1 answer option only is the poll allows multiple answers setPollAnswer chat_id:int53 message_id:int53 option_ids:vector = Ok; +//@description Returns users voted for the specified option in a non-anonymous polls. For the optimal performance the number of returned users is chosen by the library +//@chat_id Identifier of the chat to which the poll belongs @message_id Identifier of the message containing the poll +//@option_id 0-based identifier of the answer option @offset Number of users to skip in the result; must be non-negative +getPollVoters chat_id:int53 message_id:int53 option_id:int32 offset:int32 = Users; + //@description Stops a poll. A poll in a message can be stopped when the message has can_be_edited flag set //@chat_id Identifier of the chat to which the poll belongs @message_id Identifier of the message containing the poll @reply_markup The new message reply markup; for bots only stopPoll chat_id:int53 message_id:int53 reply_markup:ReplyMarkup = Ok; diff --git a/td/generate/scheme/td_api.tlo b/td/generate/scheme/td_api.tlo index 5d80ca353..338eddc9d 100644 Binary files a/td/generate/scheme/td_api.tlo and b/td/generate/scheme/td_api.tlo differ diff --git a/td/telegram/MessageContent.cpp b/td/telegram/MessageContent.cpp index 90b8ded20..cb98628c8 100644 --- a/td/telegram/MessageContent.cpp +++ b/td/telegram/MessageContent.cpp @@ -429,7 +429,7 @@ class MessageChatSetTtl : public MessageContent { class MessageUnsupported : public MessageContent { public: - static constexpr int32 CURRENT_VERSION = 4; + static constexpr int32 CURRENT_VERSION = 5; int32 version = CURRENT_VERSION; MessageUnsupported() = default; @@ -2774,18 +2774,26 @@ void set_message_content_web_page_id(MessageContent *content, WebPageId web_page static_cast(content)->web_page_id = web_page_id; } -void set_message_content_poll_answer(Td *td, MessageContent *content, FullMessageId full_message_id, +void set_message_content_poll_answer(Td *td, const MessageContent *content, FullMessageId full_message_id, vector &&option_ids, Promise &&promise) { CHECK(content->get_type() == MessageContentType::Poll); - td->poll_manager_->set_poll_answer(static_cast(content)->poll_id, full_message_id, + td->poll_manager_->set_poll_answer(static_cast(content)->poll_id, full_message_id, std::move(option_ids), std::move(promise)); } -void stop_message_content_poll(Td *td, MessageContent *content, FullMessageId full_message_id, +void get_message_content_poll_voters(Td *td, const MessageContent *content, FullMessageId full_message_id, + int32 option_id, int32 offset, + Promise>> &&promise) { + CHECK(content->get_type() == MessageContentType::Poll); + td->poll_manager_->get_poll_voters(static_cast(content)->poll_id, full_message_id, option_id, + offset, std::move(promise)); +} + +void stop_message_content_poll(Td *td, const MessageContent *content, FullMessageId full_message_id, unique_ptr &&reply_markup, Promise &&promise) { CHECK(content->get_type() == MessageContentType::Poll); - td->poll_manager_->stop_poll(static_cast(content)->poll_id, full_message_id, std::move(reply_markup), - std::move(promise)); + td->poll_manager_->stop_poll(static_cast(content)->poll_id, full_message_id, + std::move(reply_markup), std::move(promise)); } static void merge_location_access_hash(const Location &first, const Location &second) { diff --git a/td/telegram/MessageContent.h b/td/telegram/MessageContent.h index dcc188262..96420f4a9 100644 --- a/td/telegram/MessageContent.h +++ b/td/telegram/MessageContent.h @@ -190,10 +190,14 @@ WebPageId get_message_content_web_page_id(const MessageContent *content); void set_message_content_web_page_id(MessageContent *content, WebPageId web_page_id); -void set_message_content_poll_answer(Td *td, MessageContent *content, FullMessageId full_message_id, +void set_message_content_poll_answer(Td *td, const MessageContent *content, FullMessageId full_message_id, vector &&option_ids, Promise &&promise); -void stop_message_content_poll(Td *td, MessageContent *content, FullMessageId full_message_id, +void get_message_content_poll_voters(Td *td, const MessageContent *content, FullMessageId full_message_id, + int32 option_id, int32 offset, + Promise>> &&promise); + +void stop_message_content_poll(Td *td, const MessageContent *content, FullMessageId full_message_id, unique_ptr &&reply_markup, Promise &&promise); void merge_message_contents(Td *td, const MessageContent *old_content, MessageContent *new_content, diff --git a/td/telegram/MessageEntity.cpp b/td/telegram/MessageEntity.cpp index a257b95ba..8a54d1937 100644 --- a/td/telegram/MessageEntity.cpp +++ b/td/telegram/MessageEntity.cpp @@ -2595,7 +2595,7 @@ Status fix_formatted_text(string &text, vector &entities, bool al TRY_RESULT(result, clean_input_string_with_entities(text, entities)); // now entities are still sorted by offset and length, but not type, - // because some characters could be deleted and some entities bacame to end together + // because some characters could be deleted and after that some entities begin to share a common end size_t last_non_whitespace_pos; int32 last_non_whitespace_utf16_offset; diff --git a/td/telegram/MessagesManager.cpp b/td/telegram/MessagesManager.cpp index 0588cbd3c..158db251c 100644 --- a/td/telegram/MessagesManager.cpp +++ b/td/telegram/MessagesManager.cpp @@ -30118,6 +30118,28 @@ void MessagesManager::set_poll_answer(FullMessageId full_message_id, vectorcontent.get(), full_message_id, std::move(option_ids), std::move(promise)); } +void MessagesManager::get_poll_voters(FullMessageId full_message_id, int32 option_id, int32 offset, + Promise>> &&promise) { + auto m = get_message_force(full_message_id, "get_poll_voters"); + if (m == nullptr) { + return promise.set_error(Status::Error(5, "Message not found")); + } + if (!have_input_peer(full_message_id.get_dialog_id(), AccessRights::Read)) { + return promise.set_error(Status::Error(3, "Can't access the chat")); + } + if (m->content->get_type() != MessageContentType::Poll) { + return promise.set_error(Status::Error(5, "Message is not a poll")); + } + if (m->message_id.is_scheduled()) { + return promise.set_error(Status::Error(5, "Can't get poll results from scheduled messages")); + } + if (!m->message_id.is_server()) { + return promise.set_error(Status::Error(5, "Poll results can't be received")); + } + + get_message_content_poll_voters(td_, m->content.get(), full_message_id, option_id, offset, std::move(promise)); +} + void MessagesManager::stop_poll(FullMessageId full_message_id, td_api::object_ptr &&reply_markup, Promise &&promise) { auto m = get_message_force(full_message_id, "stop_poll"); diff --git a/td/telegram/MessagesManager.h b/td/telegram/MessagesManager.h index 7dd934757..8b9c0d357 100644 --- a/td/telegram/MessagesManager.h +++ b/td/telegram/MessagesManager.h @@ -803,6 +803,9 @@ class MessagesManager : public Actor { void set_poll_answer(FullMessageId full_message_id, vector &&option_ids, Promise &&promise); + void get_poll_voters(FullMessageId full_message_id, int32 option_id, int32 offset, + Promise>> &&promise); + void stop_poll(FullMessageId full_message_id, td_api::object_ptr &&reply_markup, Promise &&promise); diff --git a/td/telegram/PollManager.cpp b/td/telegram/PollManager.cpp index d9d01b81b..d3465c922 100644 --- a/td/telegram/PollManager.cpp +++ b/td/telegram/PollManager.cpp @@ -40,6 +40,7 @@ #include #include +#include namespace td { @@ -84,6 +85,54 @@ class GetPollResultsQuery : public Td::ResultHandler { } }; +class GetPollVotersQuery : public Td::ResultHandler { + Promise> promise_; + PollId poll_id_; + DialogId dialog_id_; + + public: + explicit GetPollVotersQuery(Promise> &&promise) + : promise_(std::move(promise)) { + } + + void send(PollId poll_id, FullMessageId full_message_id, BufferSlice &&option, const string &offset, int32 limit) { + poll_id_ = poll_id; + dialog_id_ = full_message_id.get_dialog_id(); + auto input_peer = td->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read); + if (input_peer == nullptr) { + LOG(INFO) << "Can't get poll, because have no read access to " << dialog_id_; + return promise_.set_error(Status::Error(400, "Chat is not accessible")); + } + + CHECK(!option.empty()); + int32 flags = telegram_api::messages_getPollVotes::OPTION_MASK; + if (!offset.empty()) { + flags |= telegram_api::messages_getPollVotes::OFFSET_MASK; + } + + auto message_id = full_message_id.get_message_id().get_server_message_id().get(); + send_query(G()->net_query_creator().create(create_storer(telegram_api::messages_getPollVotes( + flags, std::move(input_peer), message_id, std::move(option), offset, limit)))); + } + + void on_result(uint64 id, BufferSlice packet) override { + auto result_ptr = fetch_result(packet); + if (result_ptr.is_error()) { + return on_error(id, result_ptr.move_as_error()); + } + + promise_.set_value(result_ptr.move_as_ok()); + } + + void on_error(uint64 id, Status status) override { + if (!td->messages_manager_->on_get_dialog_error(dialog_id_, status, "GetPollVotersQuery") && + status.message() != "MESSAGE_ID_INVALID") { + LOG(ERROR) << "Receive " << status << ", while trying to get voters of " << poll_id_; + } + promise_.set_error(std::move(status)); + } +}; + class SetPollAnswerActor : public NetActorOnce { Promise> promise_; DialogId dialog_id_; @@ -594,6 +643,7 @@ void PollManager::set_poll_answer(PollId poll_id, FullMessageId full_message_id, return promise.set_error(Status::Error(400, "Can't retract vote in a quiz")); } + std::unordered_map affected_option_ids; vector options; for (auto &option_id : option_ids) { auto index = static_cast(option_id); @@ -601,6 +651,18 @@ void PollManager::set_poll_answer(PollId poll_id, FullMessageId full_message_id, return promise.set_error(Status::Error(400, "Invalid option ID specified")); } options.push_back(poll->options[index].data); + + affected_option_ids[index]++; + } + for (size_t option_index = 0; option_index < poll->options.size(); option_index++) { + if (poll->options[option_index].is_chosen) { + affected_option_ids[option_index]++; + } + } + for (auto it : affected_option_ids) { + if (it.second == 1) { + invalidate_poll_option_voters(poll, poll_id, it.first); + } } do_set_poll_answer(poll_id, full_message_id, std::move(options), 0, std::move(promise)); @@ -733,6 +795,157 @@ void PollManager::on_set_poll_answer(PollId poll_id, uint64 generation, } } +void PollManager::invalidate_poll_voters(const Poll *poll, PollId poll_id) { + if (poll->is_anonymous) { + return; + } + + auto it = poll_voters_.find(poll_id); + if (it == poll_voters_.end()) { + return; + } + + for (auto &voters : it->second) { + voters.was_invalidated = true; + } +} + +void PollManager::invalidate_poll_option_voters(const Poll *poll, PollId poll_id, size_t option_index) { + if (poll->is_anonymous) { + return; + } + + auto it = poll_voters_.find(poll_id); + if (it == poll_voters_.end()) { + return; + } + + auto &poll_voters = it->second; + CHECK(poll_voters.size() == poll->options.size()); + CHECK(option_index < poll_voters.size()); + poll_voters[option_index].was_invalidated = true; +} + +PollManager::PollOptionVoters &PollManager::get_poll_option_voters(const Poll *poll, PollId poll_id, int32 option_id) { + auto &poll_voters = poll_voters_[poll_id]; + if (poll_voters.empty()) { + poll_voters.resize(poll->options.size()); + } + auto index = narrow_cast(option_id); + CHECK(index < poll_voters.size()); + return poll_voters[index]; +} + +void PollManager::get_poll_voters(PollId poll_id, FullMessageId full_message_id, int32 option_id, int32 offset, + Promise>> &&promise) { + if (is_local_poll_id(poll_id)) { + return promise.set_error(Status::Error(400, "Poll results can't be received")); + } + if (offset < 0) { + return promise.set_error(Status::Error(400, "Invalid offset specified")); + } + + auto poll = get_poll(poll_id); + CHECK(poll != nullptr); + if (option_id < 0 || static_cast(option_id) >= poll->options.size()) { + return promise.set_error(Status::Error(400, "Invalid option ID specified")); + } + if (poll->is_anonymous) { + return promise.set_error(Status::Error(400, "Poll is anonymous")); + } + + auto &voters = get_poll_option_voters(poll, poll_id, option_id); + if (voters.pending_queries.empty() && voters.was_invalidated && offset == 0) { + voters.voter_user_ids.clear(); + voters.next_offset.clear(); + voters.was_invalidated = false; + } + + auto cur_offset = narrow_cast(voters.voter_user_ids.size()); + + if (offset > cur_offset) { + return promise.set_error(Status::Error(400, "Too big offset specified, voters can be received only consequently")); + } + if (offset < cur_offset) { + vector result; + for (int32 i = offset; i != cur_offset && i - offset < MAX_GET_POLL_VOTERS; i++) { + result.push_back(voters.voter_user_ids[i]); + } + return promise.set_value({poll->options[option_id].voter_count, std::move(result)}); + } + + if (poll->options[option_id].voter_count == 0) { + return promise.set_value({0, vector()}); + } + + voters.pending_queries.push_back(std::move(promise)); + if (voters.pending_queries.size() > 1) { + return; + } + + auto query_promise = PromiseCreator::lambda([actor_id = actor_id(this), poll_id, option_id]( + Result> &&result) { + send_closure(actor_id, &PollManager::on_get_poll_voters, poll_id, option_id, std::move(result)); + }); + td_->create_handler(std::move(query_promise)) + ->send(poll_id, full_message_id, BufferSlice(poll->options[option_id].data), voters.next_offset, + MAX_GET_POLL_VOTERS); +} + +void PollManager::on_get_poll_voters(PollId poll_id, int32 option_id, + Result> &&result) { + auto poll = get_poll(poll_id); + CHECK(poll != nullptr); + if (option_id < 0 || static_cast(option_id) >= poll->options.size()) { + LOG(ERROR) << "Can't process voters for option " << option_id << " in " << poll_id << ", because it has only " + << poll->options.size() << " options"; + return; + } + if (poll->is_anonymous) { + // just in case + result = Status::Error(400, "Poll is anonymous"); + } + + auto &voters = get_poll_option_voters(poll, poll_id, option_id); + auto promises = std::move(voters.pending_queries); + CHECK(!promises.empty()); + if (result.is_error()) { + for (auto &promise : promises) { + promise.set_error(result.error().clone()); + } + return; + } + + auto vote_list = result.move_as_ok(); + td_->contacts_manager_->on_get_users(std::move(vote_list->users_), "on_get_poll_voters"); + + voters.next_offset = std::move(vote_list->next_offset_); + if (poll->options[option_id].voter_count != vote_list->count_) { + ++current_generation_; + update_poll_timeout_.set_timeout_in(poll_id.get(), 0.0); + } + + vector user_ids; + for (auto &voter : vote_list->votes_) { + UserId user_id(voter->user_id_); + if (!user_id.is_valid()) { + LOG(ERROR) << "Receive " << user_id << " as voter in " << poll_id; + continue; + } + if (voter->option_ != poll->options[option_id].data) { + LOG(ERROR) << "Receive " << user_id << " in " << poll_id << " voted for unexpected option"; + continue; + } + + voters.voter_user_ids.push_back(user_id); + user_ids.push_back(user_id); + } + + for (auto &promise : promises) { + promise.set_value({vote_list->count_, vector(user_ids)}); + } +} + void PollManager::stop_poll(PollId poll_id, FullMessageId full_message_id, unique_ptr &&reply_markup, Promise &&promise) { if (is_local_poll_id(poll_id)) { @@ -1010,7 +1223,8 @@ PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptrresults_.size(); i++) { auto &poll_result = poll_results->results_[i]; Slice data = poll_result->option_.as_slice(); - for (auto &option : poll->options) { + for (size_t option_index = 0; option_index < poll->options.size(); i++) { + auto &option = poll->options[option_index]; if (option.data != data) { continue; } @@ -1028,26 +1242,28 @@ PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptr(i); } + + if (poll_result->voters_ < 0) { + LOG(ERROR) << "Receive " << poll_result->voters_ << " voters for an option in " << poll_id; + poll_result->voters_ = 0; + } + if (option.is_chosen && poll_result->voters_ == 0) { + LOG(ERROR) << "Receive 0 voters for the chosen option in " << poll_id; + poll_result->voters_ = 1; + } + if (poll_result->voters_ > poll->total_voter_count) { + LOG(ERROR) << "Have only " << poll->total_voter_count << " poll voters, but there are " << poll_result->voters_ + << " voters for an option in " << poll_id; + poll->total_voter_count = poll_result->voters_; + } + auto max_voter_count = std::numeric_limits::max() / narrow_cast(poll->options.size()) - 2; + if (poll_result->voters_ > max_voter_count) { + LOG(ERROR) << "Have too much " << poll_result->voters_ << " poll voters for an option in " << poll_id; + poll_result->voters_ = max_voter_count; + } if (poll_result->voters_ != option.voter_count) { + invalidate_poll_option_voters(poll, poll_id, option_index); option.voter_count = poll_result->voters_; - if (option.voter_count < 0) { - LOG(ERROR) << "Receive " << option.voter_count << " voters for an option in " << poll_id; - option.voter_count = 0; - } - if (option.is_chosen && option.voter_count == 0) { - LOG(ERROR) << "Receive 0 voters for the chosen option in " << poll_id; - option.voter_count = 1; - } - if (option.voter_count > poll->total_voter_count) { - LOG(ERROR) << "Have only " << poll->total_voter_count << " poll voters, but there are " << option.voter_count - << " voters for an option in " << poll_id; - poll->total_voter_count = option.voter_count; - } - auto max_voter_count = std::numeric_limits::max() / narrow_cast(poll->options.size()) - 2; - if (option.voter_count > max_voter_count) { - LOG(ERROR) << "Have too much " << option.voter_count << " poll voters for an option in " << poll_id; - option.voter_count = max_voter_count; - } is_changed = true; } } @@ -1090,6 +1306,7 @@ PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptrrecent_voter_user_ids) { poll->recent_voter_user_ids = std::move(recent_voter_user_ids); + invalidate_poll_voters(poll, poll_id); is_changed = true; } diff --git a/td/telegram/PollManager.h b/td/telegram/PollManager.h index 749f9b9a8..eec0f5d7c 100644 --- a/td/telegram/PollManager.h +++ b/td/telegram/PollManager.h @@ -58,6 +58,9 @@ class PollManager : public Actor { void set_poll_answer(PollId poll_id, FullMessageId full_message_id, vector &&option_ids, Promise &&promise); + void get_poll_voters(PollId poll_id, FullMessageId full_message_id, int32 option_id, int32 offset, + Promise>> &&promise); + void stop_poll(PollId poll_id, FullMessageId full_message_id, unique_ptr &&reply_markup, Promise &&promise); @@ -110,6 +113,15 @@ class PollManager : public Actor { void parse(ParserT &parser); }; + struct PollOptionVoters { + vector voter_user_ids; + string next_offset; + vector>>> pending_queries; + bool was_invalidated = false; // the list needs to be invalidated when voters are changed + }; + + static constexpr int32 MAX_GET_POLL_VOTERS = 20; // server side limit + class SetPollAnswerLogEvent; class StopPollLogEvent; @@ -157,6 +169,15 @@ class PollManager : public Actor { void on_set_poll_answer(PollId poll_id, uint64 generation, Result> &&result); + void invalidate_poll_voters(const Poll *poll, PollId poll_id); + + void invalidate_poll_option_voters(const Poll *poll, PollId poll_id, size_t option_index); + + PollOptionVoters &get_poll_option_voters(const Poll *poll, PollId poll_id, int32 option_id); + + void on_get_poll_voters(PollId poll_id, int32 option_id, + Result> &&result); + void do_stop_poll(PollId poll_id, FullMessageId full_message_id, unique_ptr &&reply_markup, uint64 logevent_id, Promise &&promise); @@ -177,6 +198,8 @@ class PollManager : public Actor { }; std::unordered_map pending_answers_; + std::unordered_map, PollIdHash> poll_voters_; + int64 current_local_poll_id_ = 0; uint64 current_generation_ = 0; diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index 98fc79ff9..63a87ee13 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -7253,6 +7253,21 @@ void Td::on_request(uint64 id, td_api::setPollAnswer &request) { std::move(request.option_ids_), std::move(promise)); } +void Td::on_request(uint64 id, td_api::getPollVoters &request) { + CHECK_IS_USER(); + CREATE_REQUEST_PROMISE(); + auto query_promise = PromiseCreator::lambda( + [promise = std::move(promise), td = this](Result>> result) mutable { + if (result.is_error()) { + promise.set_error(result.move_as_error()); + } else { + promise.set_value(td->contacts_manager_->get_users_object(result.ok().first, result.ok().second)); + } + }); + messages_manager_->get_poll_voters({DialogId(request.chat_id_), MessageId(request.message_id_)}, request.option_id_, + request.offset_, std::move(query_promise)); +} + void Td::on_request(uint64 id, td_api::stopPoll &request) { CREATE_OK_REQUEST_PROMISE(); messages_manager_->stop_poll({DialogId(request.chat_id_), MessageId(request.message_id_)}, diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 4380da133..a7b2ad7c8 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -907,6 +907,8 @@ class Td final : public NetQueryCallback { void on_request(uint64 id, td_api::setPollAnswer &request); + void on_request(uint64 id, td_api::getPollVoters &request); + void on_request(uint64 id, td_api::stopPoll &request); void on_request(uint64 id, const td_api::getLoginUrlInfo &request); diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index 08296ce90..59ca18a43 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -3489,6 +3489,17 @@ class CliClient final : public Actor { std::tie(message_id, option_ids) = split(args); send_request(td_api::make_object(as_chat_id(chat_id), as_message_id(message_id), to_integers(option_ids))); + } else if (op == "gpollv") { + string chat_id; + string message_id; + string option_id; + string offset; + + std::tie(chat_id, args) = split(args); + std::tie(message_id, args) = split(args); + std::tie(option_id, offset) = split(args); + send_request(td_api::make_object(as_chat_id(chat_id), as_message_id(message_id), + to_integer(option_id), to_integer(offset))); } else if (op == "stoppoll") { string chat_id; string message_id;