diff --git a/CMakeLists.txt b/CMakeLists.txt index 33d14566c..3b65d14ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -417,6 +417,7 @@ set(TDLIB_SOURCE td/telegram/PhoneNumberManager.cpp td/telegram/PrivacyManager.cpp td/telegram/Photo.cpp + td/telegram/PollManager.cpp td/telegram/ReplyMarkup.cpp td/telegram/SecretChatActor.cpp td/telegram/SecretChatDb.cpp @@ -567,6 +568,8 @@ set(TDLIB_SOURCE td/telegram/Payments.h td/telegram/PhoneNumberManager.h td/telegram/Photo.h + td/telegram/PollId.h + td/telegram/PollManager.h td/telegram/PrivacyManager.h td/telegram/PtsManager.h td/telegram/ReplyMarkup.h @@ -618,6 +621,8 @@ set(TDLIB_SOURCE td/telegram/NotificationSettings.hpp td/telegram/Payments.hpp td/telegram/Photo.hpp + td/telegram/PollId.hpp + td/telegram/PollManager.hpp td/telegram/ReplyMarkup.hpp td/telegram/SecureValue.hpp td/telegram/SendCodeHelper.hpp diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 994d15307..1893c2841 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -192,6 +192,10 @@ maskPointChin = MaskPoint; maskPosition point:MaskPoint x_shift:double y_shift:double scale:double = MaskPosition; +//@description Describes one answer of a poll @text Answer text, 1-100 characters @voter_count Number of voters for this answer @is_chosen True, if the answer was chosen by the user +pollAnswer text:string voter_count:int32 is_chosen:Bool = PollAnswer; + + //@description Describes an animation file. The animation must be encoded in GIF or MPEG4 format @duration Duration of the animation, in seconds; as defined by the sender @width Width of the animation @height Height of the animation //@file_name Original name of the file; as defined by the sender @mime_type MIME type of the file, usually "image/gif" or "video/mp4" @thumbnail Animation thumbnail; may be null @animation File containing the animation animation duration:int32 width:int32 height:int32 file_name:string mime_type:string thumbnail:photoSize animation:file = Animation; @@ -237,6 +241,9 @@ venue location:location title:string address:string provider:string id:string ty //@param_description Game description @photo Game photo @animation Game animation; may be null game id:int64 short_name:string title:string text:formattedText description:string photo:photo animation:animation = Game; +//@description Describes a poll @question Poll question, 1-255 characters @answers List of poll answers @total_voter_count Total number of voters, participating in the poll @is_closed True, if the poll is closed +poll question:string answers:vector total_voter_count:int32 is_closed:Bool = Poll; + //@description Describes a user profile photo @id Photo identifier; 0 for an empty photo. Can be used to find a photo in a list of userProfilePhotos //@small A small (160x160) user profile photo @big A big (640x640) user profile photo @@ -1215,6 +1222,9 @@ messageContact contact:contact = MessageContent; //@description A message with a game @game Game messageGame game:game = MessageContent; +//@description A message with a poll @poll Poll +messagePoll poll:poll = MessageContent; + //@description A message with an invoice from a bot @title Product title @param_description Product description @photo Product photo; may be null @currency Currency for the product price @total_amount Product total price in the minimal quantity of the currency //@start_parameter Unique invoice bot start_parameter. To share an invoice use the URL https://t.me/{bot_username}?start={start_parameter} @is_test True, if the invoice is a test invoice //@need_shipping_address True, if the shipping address should be specified @receipt_message_id The identifier of the message with the receipt, after the product has been purchased @@ -1392,6 +1402,9 @@ inputMessageGame bot_user_id:int32 game_short_name:string = InputMessageContent; //@payload The invoice payload @provider_token Payment provider token @provider_data JSON-encoded data about the invoice, which will be shared with the payment provider @start_parameter Unique invoice bot start_parameter for the generation of this invoice inputMessageInvoice invoice:invoice title:string description:string photo_url:string photo_size:int32 photo_width:int32 photo_height:int32 payload:bytes provider_token:string provider_data:string start_parameter:string = InputMessageContent; +//@description A message with a poll @question Poll question, 1-255 characters @answers List of poll answers, 1-10 strings 1-100 characters each +inputMessagePoll question:string answers:vector = InputMessageContent; + //@description A forwarded message @from_chat_id Identifier for the chat this forwarded message came from @message_id Identifier of the message to forward @in_game_share True, if a game message should be shared within a launched game; applies only to game messages inputMessageForwarded from_chat_id:int53 message_id:int53 in_game_share:Bool = InputMessageContent; diff --git a/td/generate/scheme/td_api.tlo b/td/generate/scheme/td_api.tlo index de916fe3d..ef8f376ad 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 9a1dde141..9ab0a0417 100644 --- a/td/telegram/MessageContent.cpp +++ b/td/telegram/MessageContent.cpp @@ -38,6 +38,9 @@ #include "td/telegram/Payments.hpp" #include "td/telegram/Photo.h" #include "td/telegram/Photo.hpp" +#include "td/telegram/PollId.h" +#include "td/telegram/PollId.hpp" +#include "td/telegram/PollManager.h" #include "td/telegram/secret_api.hpp" #include "td/telegram/SecureValue.h" #include "td/telegram/SecureValue.hpp" @@ -422,7 +425,7 @@ class MessageChatSetTtl : public MessageContent { class MessageUnsupported : public MessageContent { public: - static constexpr int32 CURRENT_VERSION = 2; + static constexpr int32 CURRENT_VERSION = 3; int32 version = CURRENT_VERSION; MessageUnsupported() = default; @@ -618,6 +621,19 @@ class MessagePassportDataReceived : public MessageContent { } }; +class MessagePoll : public MessageContent { + public: + PollId poll_id; + + MessagePoll() = default; + explicit MessagePoll(PollId poll_id) : poll_id(poll_id) { + } + + MessageContentType get_type() const override { + return MessageContentType::Poll; + } +}; + StringBuilder &operator<<(StringBuilder &string_builder, MessageContentType content_type) { switch (content_type) { case MessageContentType::None: @@ -702,6 +718,8 @@ StringBuilder &operator<<(StringBuilder &string_builder, MessageContentType cont return string_builder << "PassportDataSent"; case MessageContentType::PassportDataReceived: return string_builder << "PassportDataReceived"; + case MessageContentType::Poll: + return string_builder << "Poll"; default: UNREACHABLE(); return string_builder; @@ -957,6 +975,11 @@ static void store(const MessageContent *content, StorerT &storer) { store(m->credentials, storer); break; } + case MessageContentType::Poll: { + auto m = static_cast(content); + store(m->poll_id, storer); + break; + } default: UNREACHABLE(); } @@ -1283,6 +1306,13 @@ static void parse(unique_ptr &content, ParserT &parser) { content = std::move(m); break; } + case MessageContentType::Poll: { + auto m = make_unique(); + parse(m->poll_id, parser); + is_bad = !m->poll_id.is_valid(); + content = std::move(m); + break; + } default: LOG(FATAL) << "Have unknown message content type " << static_cast(content_type); } @@ -1715,6 +1745,42 @@ static Result create_input_message_content( content = std::move(message_invoice); break; } + case td_api::inputMessagePoll::ID: { + constexpr size_t MAX_POLL_QUESTION_LENGTH = 255; // server-side limit + constexpr size_t MAX_POLL_ANSWER_LENGTH = 100; // server-side limit + constexpr size_t MAX_POLL_ANSWERS = 10; // server-side limit + auto input_poll = static_cast(input_message_content.get()); + if (!clean_input_string(input_poll->question_)) { + return Status::Error(400, "Poll question must be encoded in UTF-8"); + } + if (input_poll->question_.empty()) { + return Status::Error(400, "Poll question must be non-empty"); + } + if (input_poll->question_.size() > MAX_POLL_QUESTION_LENGTH) { + return Status::Error(400, PSLICE() << "Poll question length must not exceed " << MAX_POLL_QUESTION_LENGTH); + } + if (input_poll->answers_.empty()) { + return Status::Error(400, "Poll must have at least 1 answer"); + } + if (input_poll->answers_.size() > MAX_POLL_ANSWERS) { + return Status::Error(400, PSLICE() << "Poll can't have more than " << MAX_POLL_QUESTION_LENGTH << " answers"); + } + for (auto &answer : input_poll->answers_) { + if (!clean_input_string(answer)) { + return Status::Error(400, "Poll answers must be encoded in UTF-8"); + } + if (answer.empty()) { + return Status::Error(400, "Poll answers must be non-empty"); + } + if (answer.size() > MAX_POLL_ANSWER_LENGTH) { + return Status::Error(400, PSLICE() << "Poll answers length must not exceed " << MAX_POLL_ANSWER_LENGTH); + } + } + + content = make_unique( + td->poll_manager_->create_poll(std::move(input_poll->question_), std::move(input_poll->answers_))); + break; + } default: UNREACHABLE(); } @@ -1921,6 +1987,7 @@ SecretInputMedia get_secret_input_media(const MessageContent *content, Td *td, case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: break; default: UNREACHABLE(); @@ -2069,6 +2136,10 @@ static tl_object_ptr get_input_media(const MessageCont auto m = static_cast(content); return td->voice_notes_manager_->get_input_media(m->file_id, std::move(input_file)); } + case MessageContentType::Poll: { + auto m = static_cast(content); + return td->poll_manager_->get_input_media(m->poll_id); + } case MessageContentType::Text: case MessageContentType::Unsupported: case MessageContentType::ChatCreate: @@ -2205,6 +2276,7 @@ void delete_message_content_thumbnail(MessageContent *content, Td *td) { case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: break; default: UNREACHABLE(); @@ -2254,6 +2326,7 @@ bool is_allowed_media_group_content(MessageContentType content_type) { case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return false; default: UNREACHABLE(); @@ -2318,6 +2391,7 @@ bool is_secret_message_content(int32 ttl, MessageContentType content_type) { case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return false; default: UNREACHABLE(); @@ -2345,6 +2419,7 @@ bool is_service_message_content(MessageContentType content_type) { case MessageContentType::VoiceNote: case MessageContentType::ExpiredPhoto: case MessageContentType::ExpiredVideo: + case MessageContentType::Poll: return false; case MessageContentType::ChatCreate: case MessageContentType::ChatChangeTitle: @@ -2418,6 +2493,7 @@ bool can_have_message_content_caption(MessageContentType content_type) { case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return false; default: UNREACHABLE(); @@ -2529,6 +2605,7 @@ int32 get_message_content_index_mask(const MessageContent *content, const Td *td case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return 0; default: UNREACHABLE(); @@ -3040,6 +3117,18 @@ void merge_message_contents(Td *td, MessageContent *old_content, MessageContent } break; } + case MessageContentType::Poll: { + auto old_ = static_cast(old_content); + auto new_ = static_cast(new_content); + if (old_->poll_id != new_->poll_id) { + if (old_->poll_id.get() > 0) { + LOG(ERROR) << "Poll id has changed from " << old_->poll_id << " to " << new_->poll_id; + } + // polls are updated in a different way + is_content_changed = true; + } + break; + } case MessageContentType::Unsupported: { auto old_ = static_cast(old_content); auto new_ = static_cast(new_content); @@ -3170,6 +3259,7 @@ bool merge_message_content_file_id(Td *td, MessageContent *message_content, File case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: LOG(ERROR) << "Receive new file " << new_file_id << " in a sent message of the type " << content_type; break; default: @@ -3743,10 +3833,17 @@ unique_ptr get_message_content(Td *td, FormattedText message, auto web_page_id = td->web_pages_manager_->on_get_web_page(std::move(media_web_page->webpage_), owner_dialog_id); return make_unique(std::move(message), web_page_id); } - - case telegram_api::messageMediaUnsupported::ID: { - return make_unique(); + case telegram_api::messageMediaPoll::ID: { + auto media_poll = move_tl_object_as(media); + auto poll_id = + td->poll_manager_->on_get_poll(PollId(), std::move(media_poll->poll_), std::move(media_poll->results_)); + if (!poll_id.is_valid()) { + break; + } + return make_unique(poll_id); } + case telegram_api::messageMediaUnsupported::ID: + return make_unique(); default: UNREACHABLE(); } @@ -3917,6 +4014,8 @@ unique_ptr dup_message_content(Td *td, DialogId dialog_id, const CHECK(result->file_id.is_valid()); return std::move(result); } + case MessageContentType::Poll: + return make_unique(*static_cast(content)); case MessageContentType::Unsupported: case MessageContentType::ChatCreate: case MessageContentType::ChatChangeTitle: @@ -4310,6 +4409,10 @@ tl_object_ptr get_message_content_object(const MessageCo get_encrypted_passport_element_object(td->file_manager_.get(), m->values), get_encrypted_credentials_object(m->credentials)); } + case MessageContentType::Poll: { + const MessagePoll *m = static_cast(content); + return make_tl_object(td->poll_manager_->get_poll_object(m->poll_id)); + } default: UNREACHABLE(); return nullptr; @@ -4586,6 +4689,7 @@ string get_message_content_search_text(const Td *td, const MessageContent *conte case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return string(); default: UNREACHABLE(); @@ -4758,6 +4862,8 @@ void add_message_content_dependencies(Dependencies &dependencies, const MessageC break; case MessageContentType::PassportDataReceived: break; + case MessageContentType::Poll: + break; default: UNREACHABLE(); break; diff --git a/td/telegram/MessageContent.h b/td/telegram/MessageContent.h index 8ce2e4b71..3a2baef5f 100644 --- a/td/telegram/MessageContent.h +++ b/td/telegram/MessageContent.h @@ -74,7 +74,8 @@ enum class MessageContentType : int32 { CustomServiceAction, WebsiteConnected, PassportDataSent, - PassportDataReceived + PassportDataReceived, + Poll }; StringBuilder &operator<<(StringBuilder &string_builder, MessageContentType content_type); diff --git a/td/telegram/MessagesManager.cpp b/td/telegram/MessagesManager.cpp index 8d292a1e7..150ac88b8 100644 --- a/td/telegram/MessagesManager.cpp +++ b/td/telegram/MessagesManager.cpp @@ -5185,6 +5185,7 @@ bool MessagesManager::need_cancel_user_dialog_action(int32 action_id, MessageCon case MessageContentType::WebsiteConnected: case MessageContentType::PassportDataSent: case MessageContentType::PassportDataReceived: + case MessageContentType::Poll: return false; default: UNREACHABLE(); @@ -9510,10 +9511,8 @@ std::pair> MessagesManager::creat } if (sender_user_id.is_valid() && (sender_user_id == my_id && dialog_id != my_dialog_id) != is_outgoing) { - // if (content->get_type() != MessageContentType::ChatAddUser) { // TODO: we have wrong flags for invites via links LOG(ERROR) << "Receive wrong message out flag: me is " << my_id << ", message is from " << sender_user_id << ", flags = " << flags << " for " << message_id << " in " << dialog_id; - // } is_outgoing = !is_outgoing; if (dialog_type == DialogType::Channel && !running_get_difference_ && !running_get_channel_difference(dialog_id) && @@ -14825,6 +14824,7 @@ Status MessagesManager::can_send_message_content(DialogId dialog_id, const Messa bool can_send_stickers = true; bool can_send_animations = true; bool can_send_games = true; + bool can_send_polls = true; switch (dialog_type) { case DialogType::User: @@ -14838,10 +14838,12 @@ Status MessagesManager::can_send_message_content(DialogId dialog_id, const Messa can_send_stickers = channel_status.can_send_stickers(); can_send_animations = channel_status.can_send_animations(); can_send_games = channel_status.can_send_games(); + // can_send_polls = channel_status.can_send_polls(); TODO break; } case DialogType::SecretChat: can_send_games = false; + can_send_polls = false; break; case DialogType::None: default: @@ -14924,6 +14926,11 @@ Status MessagesManager::can_send_message_content(DialogId dialog_id, const Messa return Status::Error(400, "Not enough rights to send photos to the chat"); } break; + case MessageContentType::Poll: + if (!can_send_polls) { + return Status::Error(400, "Not enough rights to send polls to the chat"); + } + break; case MessageContentType::Sticker: if (!can_send_stickers) { return Status::Error(400, "Not enough rights to send stickers to the chat"); @@ -16225,6 +16232,7 @@ bool MessagesManager::can_edit_message(DialogId dialog_id, const Message *m, boo } case MessageContentType::Contact: case MessageContentType::Location: + case MessageContentType::Poll: case MessageContentType::Sticker: case MessageContentType::Venue: case MessageContentType::VideoNote: @@ -19275,6 +19283,8 @@ void MessagesManager::on_send_message_fail(int64 random_id, Status error) { error_message = "Wrong game short name specified"; } else if (content_type == MessageContentType::Invoice) { error_message = "Wrong invoice information specified"; + } else if (content_type == MessageContentType::Poll) { + error_message = "Wrong poll data specified"; } else { error_message = "Wrong file identifier/HTTP URL specified"; } diff --git a/td/telegram/PollId.h b/td/telegram/PollId.h new file mode 100644 index 000000000..d59108a6b --- /dev/null +++ b/td/telegram/PollId.h @@ -0,0 +1,56 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2019 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#pragma once + +#include "td/utils/common.h" +#include "td/utils/StringBuilder.h" +#include "td/utils/tl_helpers.h" + +#include +#include + +namespace td { + +class PollId { + int64 id = 0; + + public: + PollId() = default; + + explicit PollId(int64 poll_id) : id(poll_id) { + } + template ::value>> + PollId(T poll_id) = delete; + + int64 get() const { + return id; + } + + bool operator==(const PollId &other) const { + return id == other.id; + } + + bool operator!=(const PollId &other) const { + return id != other.id; + } + + bool is_valid() const { + return id != 0; + } +}; + +struct PollIdHash { + std::size_t operator()(PollId poll_id) const { + return std::hash()(poll_id.get()); + } +}; + +inline StringBuilder &operator<<(StringBuilder &string_builder, PollId poll_id) { + return string_builder << "poll " << poll_id.get(); +} + +} // namespace td diff --git a/td/telegram/PollId.hpp b/td/telegram/PollId.hpp new file mode 100644 index 000000000..93021407b --- /dev/null +++ b/td/telegram/PollId.hpp @@ -0,0 +1,27 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2019 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#pragma once + +#include "td/telegram/PollId.h" + +#include "td/telegram/PollManager.h" +#include "td/telegram/PollManager.hpp" +#include "td/telegram/Td.h" + +namespace td { + +template +void store(const PollId &poll_id, StorerT &storer) { + storer.context()->td().get_actor_unsafe()->poll_manager_->store_poll(poll_id, storer); +} + +template +void parse(PollId &poll_id, ParserT &parser) { + poll_id = parser.context()->td().get_actor_unsafe()->poll_manager_->parse_poll(parser); +} + +} // namespace td diff --git a/td/telegram/PollManager.cpp b/td/telegram/PollManager.cpp new file mode 100644 index 000000000..3e356eac2 --- /dev/null +++ b/td/telegram/PollManager.cpp @@ -0,0 +1,254 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2019 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include "td/telegram/PollManager.h" + +#include "td/telegram/Global.h" +#include "td/telegram/logevent/LogEvent.h" +#include "td/telegram/PollManager.hpp" +#include "td/telegram/TdDb.h" +#include "td/telegram/Td.h" + +#include "td/db/SqliteKeyValue.h" +#include "td/db/SqliteKeyValueAsync.h" + +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/Status.h" + +namespace td { + +PollManager::PollManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { +} + +void PollManager::tear_down() { + parent_.reset(); +} + +PollManager::~PollManager() = default; + +bool PollManager::is_local_poll_id(PollId poll_id) { + return poll_id.get() < 0 && poll_id.get() > std::numeric_limits::min(); +} + +const PollManager::Poll *PollManager::get_poll(PollId poll_id) const { + auto p = polls_.find(poll_id); + if (p == polls_.end()) { + return nullptr; + } else { + return p->second.get(); + } +} + +PollManager::Poll *PollManager::get_poll_editable(PollId poll_id) { + auto p = polls_.find(poll_id); + if (p == polls_.end()) { + return nullptr; + } else { + return p->second.get(); + } +} + +bool PollManager::have_poll(PollId poll_id) const { + return get_poll(poll_id) != nullptr; +} + +string PollManager::get_poll_database_key(PollId poll_id) { + return PSTRING() << "poll" << poll_id.get(); +} + +void PollManager::save_poll(const Poll *poll, PollId poll_id) { + if (!G()->parameters().use_message_db) { + return; + } + CHECK(!is_local_poll_id(poll_id)); + + LOG(INFO) << "Save " << poll_id << " to database"; + CHECK(poll != nullptr); + // G()->td_db()->get_sqlite_pmc()->set(get_poll_database_key(poll_id), log_event_store(*poll).as_slice().str(), Auto()); +} + +void PollManager::on_load_poll_from_database(PollId poll_id, string value) { + loaded_from_database_polls_.insert(poll_id); + + LOG(INFO) << "Successfully loaded " << poll_id << " of size " << value.size() << " from database"; + // G()->td_db()->get_sqlite_pmc()->erase(get_poll_database_key(poll_id), Auto()); + // return; + + CHECK(!have_poll(poll_id)); + if (!value.empty()) { + auto result = make_unique(); + auto status = log_event_parse(*result, value); + if (status.is_error()) { + LOG(FATAL) << status << ": " << format::as_hex_dump<4>(Slice(value)); + } + polls_[poll_id] = std::move(result); + } +} + +bool PollManager::have_poll_force(PollId poll_id) { + return get_poll_force(poll_id) != nullptr; +} + +PollManager::Poll *PollManager::get_poll_force(PollId poll_id) { + auto poll = get_poll_editable(poll_id); + if (poll != nullptr) { + return poll; + } + if (!G()->parameters().use_message_db) { + return nullptr; + } + if (loaded_from_database_polls_.count(poll_id)) { + return nullptr; + } + + LOG(INFO) << "Trying to load " << poll_id << " from database"; + on_load_poll_from_database(poll_id, G()->td_db()->get_sqlite_sync_pmc()->get(get_poll_database_key(poll_id))); + return get_poll_editable(poll_id); +} + +td_api::object_ptr PollManager::get_poll_answer_object(const PollAnswer &poll_answer) { + return td_api::make_object(poll_answer.text, poll_answer.voter_count, poll_answer.is_chosen); +} + +td_api::object_ptr PollManager::get_poll_object(PollId poll_id) const { + auto poll = get_poll(poll_id); + CHECK(poll != nullptr); + return td_api::make_object(poll->question, transform(poll->answers, get_poll_answer_object), + poll->total_voter_count, poll->is_closed); +} + +telegram_api::object_ptr PollManager::get_input_poll_answer(const PollAnswer &poll_answer) { + return telegram_api::make_object(poll_answer.text, BufferSlice(poll_answer.data)); +} + +PollId PollManager::create_poll(string &&question, vector &&answers) { + auto poll = make_unique(); + poll->question = std::move(question); + int pos = 0; + for (auto &answer_text : answers) { + PollAnswer answer; + answer.text = std::move(answer_text); + answer.data = to_string(pos++); + poll->answers.push_back(std::move(answer)); + } + + PollId poll_id(--current_local_poll_id_); + CHECK(is_local_poll_id(poll_id)); + bool is_inserted = polls_.emplace(poll_id, std::move(poll)).second; + CHECK(is_inserted); + return poll_id; +} + +tl_object_ptr PollManager::get_input_media(PollId poll_id) const { + auto poll = get_poll(poll_id); + CHECK(poll != nullptr); + return telegram_api::make_object(telegram_api::make_object( + 0, 0, false /* ignored */, poll->question, transform(poll->answers, get_input_poll_answer))); +} + +vector PollManager::get_poll_answers( + vector> &&poll_answers) { + return transform(std::move(poll_answers), [](tl_object_ptr &&poll_answer) { + PollAnswer answer; + answer.text = std::move(poll_answer->text_); + answer.data = poll_answer->option_.as_slice().str(); + return answer; + }); +} + +PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptr &&poll_server, + tl_object_ptr &&poll_results) { + if (!poll_id.is_valid() && poll_server != nullptr) { + poll_id = PollId(poll_server->id_); + } + if (!poll_id.is_valid() || is_local_poll_id(poll_id)) { + LOG(ERROR) << "Receive " << poll_id << " from server"; + return PollId(); + } + if (poll_server != nullptr && poll_server->id_ != poll_id.get()) { + LOG(ERROR) << "Receive poll " << poll_server->id_ << " instead of " << poll_id; + return PollId(); + } + + auto poll = get_poll_force(poll_id); + bool is_changed = false; + if (poll == nullptr) { + if (poll_server == nullptr) { + LOG(INFO) << "Ignore " << poll_id << ", because have no data about it"; + return PollId(); + } + + auto p = make_unique(); + poll = p.get(); + bool is_inserted = polls_.emplace(poll_id, std::move(p)).second; + CHECK(is_inserted); + } + CHECK(poll != nullptr); + + if (poll_server != nullptr) { + if (poll->question != poll_server->question_) { + poll->question = std::move(poll_server->question_); + is_changed = true; + } + if (poll->answers.size() != poll_server->answers_.size()) { + poll->answers = get_poll_answers(std::move(poll_server->answers_)); + is_changed = true; + } else { + for (size_t i = 0; i < poll->answers.size(); i++) { + if (poll->answers[i].text != poll_server->answers_[i]->text_) { + poll->answers[i].text = std::move(poll_server->answers_[i]->text_); + is_changed = true; + } + if (poll->answers[i].data != poll_server->answers_[i]->option_.as_slice()) { + poll->answers[i].data = poll_server->answers_[i]->option_.as_slice().str(); + poll->answers[i].voter_count = 0; + poll->answers[i].is_chosen = false; + is_changed = true; + } + } + } + bool is_closed = (poll_server->flags_ & telegram_api::poll::CLOSED_MASK) != 0; + if (is_closed != poll->is_closed) { + poll->is_closed = is_closed; + is_changed = true; + } + } + + CHECK(poll_results != nullptr); + bool is_min = (poll_results->flags_ & telegram_api::pollResults::MIN_MASK) != 0; + if ((poll_results->flags_ & telegram_api::pollResults::TOTAL_VOTERS_MASK) != 0 && + poll_results->total_voters_ != poll->total_voter_count) { + poll->total_voter_count = poll_results->total_voters_; + is_changed = true; + } + for (auto &poll_result : poll_results->results_) { + Slice data = poll_result->option_.as_slice(); + for (auto &answer : poll->answers) { + if (answer.data != data) { + continue; + } + if (!is_min) { + bool is_chosen = (poll_result->flags_ & telegram_api::pollAnswerVoters::CHOSEN_MASK) != 0; + if (is_chosen != answer.is_chosen) { + answer.is_chosen = is_chosen; + is_changed = true; + } + } + if (poll_result->voters_ != answer.voter_count) { + answer.voter_count = poll_result->voters_; + is_changed = true; + } + } + } + + if (is_changed) { + save_poll(poll, poll_id); + } + return poll_id; +} + +} // namespace td diff --git a/td/telegram/PollManager.h b/td/telegram/PollManager.h new file mode 100644 index 000000000..08a75f709 --- /dev/null +++ b/td/telegram/PollManager.h @@ -0,0 +1,110 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2019 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#pragma once + +#include "td/telegram/PollId.h" +#include "td/telegram/td_api.h" +#include "td/telegram/telegram_api.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/utils/common.h" + +#include +#include + +namespace td { + +class Td; + +class PollManager : public Actor { + public: + PollManager(Td *td, ActorShared<> parent); + + PollManager(const PollManager &) = delete; + PollManager &operator=(const PollManager &) = delete; + PollManager(PollManager &&) = delete; + PollManager &operator=(PollManager &&) = delete; + ~PollManager() override; + + PollId create_poll(string &&question, vector &&answers); + + tl_object_ptr get_input_media(PollId poll_id) const; + + PollId on_get_poll(PollId poll_id, tl_object_ptr &&poll_server, + tl_object_ptr &&poll_results); + + td_api::object_ptr get_poll_object(PollId poll_id) const; + + template + void store_poll(PollId poll_id, StorerT &storer) const; + + template + PollId parse_poll(ParserT &parser); + + private: + struct PollAnswer { + string text; + string data; + int32 voter_count = 0; + bool is_chosen = false; + + template + void store(StorerT &storer) const; + template + void parse(ParserT &parser); + }; + + struct Poll { + string question; + vector answers; + int32 total_voter_count = 0; + bool is_closed = false; + + template + void store(StorerT &storer) const; + template + void parse(ParserT &parser); + }; + + void tear_down() override; + + static bool is_local_poll_id(PollId poll_id); + + static td_api::object_ptr get_poll_answer_object(const PollAnswer &poll_answer); + + static telegram_api::object_ptr get_input_poll_answer(const PollAnswer &poll_answer); + + static vector get_poll_answers(vector> &&poll_answers); + + bool have_poll(PollId poll_id) const; + + bool have_poll_force(PollId poll_id); + + const Poll *get_poll(PollId poll_id) const; + + Poll *get_poll_editable(PollId poll_id); + + static string get_poll_database_key(PollId poll_id); + + void save_poll(const Poll *poll, PollId poll_id); + + void on_load_poll_from_database(PollId poll_id, string value); + + Poll *get_poll_force(PollId poll_id); + + Td *td_; + ActorShared<> parent_; + std::unordered_map, PollIdHash> polls_; + + int64 current_local_poll_id_ = 0; + + std::unordered_set loaded_from_database_polls_; +}; + +} // namespace td diff --git a/td/telegram/PollManager.hpp b/td/telegram/PollManager.hpp new file mode 100644 index 000000000..fb331f95c --- /dev/null +++ b/td/telegram/PollManager.hpp @@ -0,0 +1,97 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2019 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#pragma once + +#include "td/telegram/PollManager.h" + +#include "td/utils/common.h" +#include "td/utils/misc.h" +#include "td/utils/tl_helpers.h" + +namespace td { + +template +void PollManager::PollAnswer::store(StorerT &storer) const { + using ::td::store; + BEGIN_STORE_FLAGS(); + STORE_FLAG(is_chosen); + END_STORE_FLAGS(); + + store(text, storer); + store(data, storer); + store(voter_count, storer); +} + +template +void PollManager::PollAnswer::parse(ParserT &parser) { + using ::td::parse; + BEGIN_PARSE_FLAGS(); + PARSE_FLAG(is_chosen); + END_PARSE_FLAGS(); + + parse(text, parser); + parse(data, parser); + parse(voter_count, parser); +} + +template +void PollManager::Poll::store(StorerT &storer) const { + using ::td::store; + BEGIN_STORE_FLAGS(); + STORE_FLAG(is_closed); + END_STORE_FLAGS(); + + store(question, storer); + store(answers, storer); + store(total_voter_count, storer); +} + +template +void PollManager::Poll::parse(ParserT &parser) { + using ::td::parse; + BEGIN_PARSE_FLAGS(); + PARSE_FLAG(is_closed); + END_PARSE_FLAGS(); + + parse(question, parser); + parse(answers, parser); + parse(total_voter_count, parser); +} + +template +void PollManager::store_poll(PollId poll_id, StorerT &storer) const { + td::store(poll_id.get(), storer); + if (is_local_poll_id(poll_id)) { + auto poll = get_poll(poll_id); + CHECK(poll != nullptr); + store(poll->question, storer); + vector answers = transform(poll->answers, [](const PollAnswer &answer) { return answer.text; }); + store(answers, storer); + } +} + +template +PollId PollManager::parse_poll(ParserT &parser) { + int64 poll_id_int; + td::parse(poll_id_int, parser); + PollId poll_id(poll_id_int); + if (is_local_poll_id(poll_id)) { + string question; + vector answers; + parse(question, parser); + parse(answers, parser); + return create_poll(std::move(question), std::move(answers)); + } + + auto poll = get_poll_force(poll_id); + if (poll == nullptr) { + return PollId(); + } + return poll_id; +} + +} // namespace td diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index 7c85878ff..660caec12 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -55,6 +55,7 @@ #include "td/telegram/Payments.h" #include "td/telegram/PhoneNumberManager.h" #include "td/telegram/Photo.h" +#include "td/telegram/PollManager.h" #include "td/telegram/PrivacyManager.h" #include "td/telegram/RequestActor.h" #include "td/telegram/SecretChatId.h" @@ -3679,6 +3680,8 @@ void Td::dec_actor_refcnt() { LOG(DEBUG) << "MessagesManager was cleared " << timer; notification_manager_.reset(); LOG(DEBUG) << "NotificationManager was cleared " << timer; + poll_manager_.reset(); + LOG(DEBUG) << "PollManager was cleared " << timer; stickers_manager_.reset(); LOG(DEBUG) << "StickersManager was cleared " << timer; updates_manager_.reset(); @@ -3854,6 +3857,8 @@ void Td::clear() { LOG(DEBUG) << "MessagesManager actor was cleared " << timer; notification_manager_actor_.reset(); LOG(DEBUG) << "NotificationManager actor was cleared " << timer; + poll_manager_actor_.reset(); + LOG(DEBUG) << "PollManager actor was cleared " << timer; stickers_manager_actor_.reset(); LOG(DEBUG) << "StickersManager actor was cleared " << timer; updates_manager_actor_.reset(); @@ -4182,6 +4187,8 @@ Status Td::init(DbKey key) { G()->set_messages_manager(messages_manager_actor_.get()); notification_manager_ = make_unique(this, create_reference()); notification_manager_actor_ = register_actor("NotificationManager", notification_manager_.get()); + poll_manager_ = make_unique(this, create_reference()); + poll_manager_actor_ = register_actor("PollManager", poll_manager_.get()); G()->set_notification_manager(notification_manager_actor_.get()); stickers_manager_ = make_unique(this, create_reference()); stickers_manager_actor_ = register_actor("StickersManager", stickers_manager_.get()); diff --git a/td/telegram/Td.h b/td/telegram/Td.h index 140be9e9c..e135397e3 100644 --- a/td/telegram/Td.h +++ b/td/telegram/Td.h @@ -54,6 +54,7 @@ class NetStatsManager; class NotificationManager; class PasswordManager; class PhoneNumberManager; +class PollManager; class PrivacyManager; class SecureManager; class SecretChatsManager; @@ -149,6 +150,8 @@ class Td final : public NetQueryCallback { ActorOwn messages_manager_actor_; unique_ptr notification_manager_; ActorOwn notification_manager_actor_; + unique_ptr poll_manager_; + ActorOwn poll_manager_actor_; unique_ptr stickers_manager_; ActorOwn stickers_manager_actor_; unique_ptr updates_manager_; diff --git a/td/telegram/UpdatesManager.cpp b/td/telegram/UpdatesManager.cpp index f66e6a589..6ede0adef 100644 --- a/td/telegram/UpdatesManager.cpp +++ b/td/telegram/UpdatesManager.cpp @@ -27,6 +27,7 @@ #include "td/telegram/net/NetQuery.h" #include "td/telegram/NotificationManager.h" #include "td/telegram/Payments.h" +#include "td/telegram/PollManager.h" #include "td/telegram/PrivacyManager.h" #include "td/telegram/SecretChatId.h" #include "td/telegram/SecretChatsManager.h" @@ -1869,9 +1870,10 @@ void UpdatesManager::on_update(tl_object_ptr updat std::move(update->difference_)); } -// unsupported updates - void UpdatesManager::on_update(tl_object_ptr update, bool /*force_apply*/) { + td_->poll_manager_->on_get_poll(PollId(update->poll_id_), std::move(update->poll_), std::move(update->results_)); } +// unsupported updates + } // namespace td diff --git a/td/telegram/UpdatesManager.h b/td/telegram/UpdatesManager.h index 2c6395d2c..b67da3ae3 100644 --- a/td/telegram/UpdatesManager.h +++ b/td/telegram/UpdatesManager.h @@ -280,8 +280,9 @@ class UpdatesManager : public Actor { void on_update(tl_object_ptr update, bool /*force_apply*/); void on_update(tl_object_ptr update, bool /*force_apply*/); - // unsupported updates void on_update(tl_object_ptr update, bool /*force_apply*/); + + // unsupported updates }; } // namespace td diff --git a/td/telegram/WebPageId.h b/td/telegram/WebPageId.h index 392cc9dce..e8e4aefbe 100644 --- a/td/telegram/WebPageId.h +++ b/td/telegram/WebPageId.h @@ -21,10 +21,10 @@ class WebPageId { public: WebPageId() = default; - explicit WebPageId(int64 webpage_id) : id(webpage_id) { + explicit WebPageId(int64 web_page_id) : id(web_page_id) { } template ::value>> - WebPageId(T webpage_id) = delete; + WebPageId(T web_page_id) = delete; int64 get() const { return id; @@ -54,13 +54,13 @@ class WebPageId { }; struct WebPageIdHash { - std::size_t operator()(WebPageId webpage_id) const { - return std::hash()(webpage_id.get()); + std::size_t operator()(WebPageId web_page_id) const { + return std::hash()(web_page_id.get()); } }; -inline StringBuilder &operator<<(StringBuilder &string_builder, WebPageId webpage_id) { - return string_builder << "web page " << webpage_id.get(); +inline StringBuilder &operator<<(StringBuilder &string_builder, WebPageId web_page_id) { + return string_builder << "web page " << web_page_id.get(); } } // namespace td diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index 029fbdb83..9f494b9ba 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -2975,6 +2975,14 @@ class CliClient final : public Actor { send_message(chat_id, make_tl_object(as_location(latitude, longitude), to_integer(period))); + } else if (op == "spoll") { + string chat_id; + string question; + std::tie(chat_id, args) = split(args); + std::tie(question, args) = split(args); + auto answers = full_split(args); + + send_message(chat_id, make_tl_object(question, std::move(answers))); } else if (op == "sp") { string chat_id; string photo_path; diff --git a/td/telegram/files/FileManager.h b/td/telegram/files/FileManager.h index 6c0bb5746..6558d2240 100644 --- a/td/telegram/files/FileManager.h +++ b/td/telegram/files/FileManager.h @@ -441,11 +441,11 @@ class FileManager : public FileLoadManager::Callback { static bool extract_was_uploaded(const tl_object_ptr &input_chat_photo); static string extract_file_reference(const tl_object_ptr &input_chat_photo); - template - void store_file(FileId file_id, T &storer, int32 ttl = 5) const; + template + void store_file(FileId file_id, StorerT &storer, int32 ttl = 5) const; - template - FileId parse_file(T &parser); + template + FileId parse_file(StorerT &parser); private: static constexpr char PERSISTENT_ID_VERSION = 2;