From 7be2966e2731bccc332ef219f52e217eceba4ba8 Mon Sep 17 00:00:00 2001 From: levlam Date: Tue, 23 Apr 2024 03:10:58 +0300 Subject: [PATCH] Support custom emoji in poll question and options. --- td/generate/scheme/td_api.tl | 14 +++---- td/telegram/MessageContent.cpp | 30 ++++++--------- td/telegram/MessageEntity.cpp | 5 ++- td/telegram/PollManager.cpp | 52 +++++++++++++++---------- td/telegram/PollManager.h | 12 +++--- td/telegram/PollManager.hpp | 70 +++++++++++++++++++++++++++++----- td/telegram/cli.cpp | 10 +++-- 7 files changed, 126 insertions(+), 67 deletions(-) diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index baa29eac2..0774e0ea6 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -350,12 +350,12 @@ closedVectorPath commands:vector = ClosedVectorPath; //@description Describes one answer option of a poll -//@text Option text; 1-100 characters +//@text Option text; 1-100 characters. Only custom emoji entities are allowed //@voter_count Number of voters for this option, available only for closed or voted polls //@vote_percentage The percentage of votes for this option; 0-100 //@is_chosen True, if the option was chosen by the user //@is_being_chosen True, if the option is being chosen by a pending setPollAnswer request -pollOption text:string voter_count:int32 vote_percentage:int32 is_chosen:Bool is_being_chosen:Bool = PollOption; +pollOption text:formattedText voter_count:int32 vote_percentage:int32 is_chosen:Bool is_being_chosen:Bool = PollOption; //@class PollType @description Describes the type of poll @@ -502,7 +502,7 @@ webApp short_name:string title:string description:string photo:photo animation:a //@description Describes a poll //@id Unique poll identifier -//@question Poll question; 1-300 characters +//@question Poll question; 1-300 characters. Only custom emoji entities are allowed //@options List of poll answer options //@total_voter_count Total number of voters, participating in the poll //@recent_voter_ids Identifiers of recent voters, if the poll is non-anonymous @@ -511,7 +511,7 @@ webApp short_name:string title:string description:string photo:photo animation:a //@open_period Amount of time the poll will be active after creation, in seconds //@close_date Point in time (Unix timestamp) when the poll will automatically be closed //@is_closed True, if the poll is closed -poll id:int64 question:string options:vector total_voter_count:int32 recent_voter_ids:vector is_anonymous:Bool type:PollType open_period:int32 close_date:int32 is_closed:Bool = Poll; +poll id:int64 question:formattedText options:vector total_voter_count:int32 recent_voter_ids:vector is_anonymous:Bool type:PollType open_period:int32 close_date:int32 is_closed:Bool = Poll; //@description Describes a chat background @@ -3473,14 +3473,14 @@ inputMessageGame bot_user_id:int53 game_short_name:string = InputMessageContent; 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 extended_media_content:InputMessageContent = InputMessageContent; //@description A message with a poll. Polls can't be sent to secret chats. Polls can be sent only to a private chat with a bot -//@question Poll question; 1-255 characters (up to 300 characters for bots) -//@options List of poll answer options, 2-10 strings 1-100 characters each +//@question Poll question; 1-255 characters (up to 300 characters for bots). Only custom emoji entities are allowed to be added and only by Premium users +//@options List of poll answer options, 2-10 strings 1-100 characters each. Only custom emoji entities are allowed to be added and only by Premium users //@is_anonymous True, if the poll voters are anonymous. Non-anonymous polls can't be sent or forwarded to channels //@type Type of the poll //@open_period Amount of time the poll will be active after creation, in seconds; for bots only //@close_date Point in time (Unix timestamp) when the poll will automatically be closed; for bots only //@is_closed True, if the poll needs to be sent already closed; for bots only -inputMessagePoll question:string options:vector is_anonymous:Bool type:PollType open_period:int32 close_date:int32 is_closed:Bool = InputMessageContent; +inputMessagePoll question:formattedText options:vector is_anonymous:Bool type:PollType open_period:int32 close_date:int32 is_closed:Bool = InputMessageContent; //@description A message with a forwarded story. Stories can't be sent to secret chats. A story can be forwarded only if story.can_be_forwarded //@story_sender_chat_id Identifier of the chat that posted the story diff --git a/td/telegram/MessageContent.cpp b/td/telegram/MessageContent.cpp index 8b34fc3ae..45c720b8c 100644 --- a/td/telegram/MessageContent.cpp +++ b/td/telegram/MessageContent.cpp @@ -2801,13 +2801,9 @@ static Result create_input_message_content( constexpr size_t MAX_POLL_OPTION_LENGTH = 100; // server-side limit constexpr size_t MAX_POLL_OPTIONS = 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 (utf8_length(input_poll->question_) > MAX_POLL_QUESTION_LENGTH) { + TRY_RESULT(question, + get_formatted_text(td, dialog_id, std::move(input_poll->question_), is_bot, false, true, false)); + if (utf8_length(question.text) > MAX_POLL_QUESTION_LENGTH) { return Status::Error(400, PSLICE() << "Poll question length must not exceed " << MAX_POLL_QUESTION_LENGTH); } if (input_poll->options_.size() <= 1) { @@ -2816,16 +2812,13 @@ static Result create_input_message_content( if (input_poll->options_.size() > MAX_POLL_OPTIONS) { return Status::Error(400, PSLICE() << "Poll can't have more than " << MAX_POLL_OPTIONS << " options"); } - for (auto &option : input_poll->options_) { - if (!clean_input_string(option)) { - return Status::Error(400, "Poll options must be encoded in UTF-8"); - } - if (option.empty()) { - return Status::Error(400, "Poll options must be non-empty"); - } - if (utf8_length(option) > MAX_POLL_OPTION_LENGTH) { + vector options; + for (auto &input_option : input_poll->options_) { + TRY_RESULT(option, get_formatted_text(td, dialog_id, std::move(input_option), is_bot, false, true, false)); + if (utf8_length(option.text) > MAX_POLL_OPTION_LENGTH) { return Status::Error(400, PSLICE() << "Poll options length must not exceed " << MAX_POLL_OPTION_LENGTH); } + options.push_back(std::move(option)); } bool allow_multiple_answers = false; @@ -2862,10 +2855,9 @@ static Result create_input_message_content( close_date = 0; } bool is_closed = is_bot ? input_poll->is_closed_ : false; - content = make_unique( - td->poll_manager_->create_poll(std::move(input_poll->question_), std::move(input_poll->options_), - input_poll->is_anonymous_, allow_multiple_answers, is_quiz, correct_option_id, - std::move(explanation), open_period, close_date, is_closed)); + content = make_unique(td->poll_manager_->create_poll( + std::move(question), std::move(options), input_poll->is_anonymous_, allow_multiple_answers, is_quiz, + correct_option_id, std::move(explanation), open_period, close_date, is_closed)); break; } case td_api::inputMessageStory::ID: { diff --git a/td/telegram/MessageEntity.cpp b/td/telegram/MessageEntity.cpp index 2fd150fc5..5aa549fd0 100644 --- a/td/telegram/MessageEntity.cpp +++ b/td/telegram/MessageEntity.cpp @@ -4351,7 +4351,7 @@ Status fix_formatted_text(string &text, vector &entities, bool al entities.clear(); return Status::OK(); } - return Status::Error(400, "Message must be non-empty"); + return Status::Error(400, "Text must be non-empty"); } // re-fix entities if needed after removal of some characters @@ -4407,7 +4407,7 @@ Status fix_formatted_text(string &text, vector &entities, bool al LOG_CHECK(check_utf8(text)) << text; if (!allow_empty && is_empty_string(text)) { - return Status::Error(400, "Message must be non-empty"); + return Status::Error(400, "Text must be non-empty"); } constexpr size_t LENGTH_LIMIT = 35000; // server side limit @@ -4640,6 +4640,7 @@ vector> get_input_message_entities(co make_tl_object(entity.offset, entity.length, entity.argument)); break; case MessageEntity::Type::MentionName: { + CHECK(user_manager != nullptr); auto input_user = user_manager->get_input_user_force(entity.user_id); result.push_back(make_tl_object(entity.offset, entity.length, std::move(input_user))); diff --git a/td/telegram/PollManager.cpp b/td/telegram/PollManager.cpp index d74000ed2..c487b375a 100644 --- a/td/telegram/PollManager.cpp +++ b/td/telegram/PollManager.cpp @@ -428,9 +428,13 @@ PollManager::Poll *PollManager::get_poll_force(PollId poll_id) { return get_poll_editable(poll_id); } +void PollManager::remove_unallowed_entities(FormattedText &text) { + td::remove_if(text.entities, [](MessageEntity &entity) { return entity.type != MessageEntity::Type::CustomEmoji; }); +} + td_api::object_ptr PollManager::get_poll_option_object(const PollOption &poll_option) { - return td_api::make_object(poll_option.text_, poll_option.voter_count_, 0, poll_option.is_chosen_, - false); + return td_api::make_object(get_formatted_text_object(poll_option.text_, true, -1), + poll_option.voter_count_, 0, poll_option.is_chosen_, false); } vector PollManager::get_vote_percentage(const vector &voter_counts, int32 total_voter_count) { @@ -558,8 +562,8 @@ td_api::object_ptr PollManager::get_poll_object(PollId poll_id, co voter_count_diff = -1; } poll_options.push_back(td_api::make_object( - poll_option.text_, poll_option.voter_count_ - static_cast(poll_option.is_chosen_), 0, false, - is_being_chosen)); + get_formatted_text_object(poll_option.text_, true, -1), + poll_option.voter_count_ - static_cast(poll_option.is_chosen_), 0, false, is_being_chosen)); } } @@ -632,20 +636,24 @@ td_api::object_ptr PollManager::get_poll_object(PollId poll_id, co recent_voters.push_back(std::move(recent_voter)); } } - return td_api::make_object(poll_id.get(), poll->question_, std::move(poll_options), total_voter_count, - std::move(recent_voters), poll->is_anonymous_, std::move(poll_type), - open_period, close_date, poll->is_closed_); + return td_api::make_object( + poll_id.get(), get_formatted_text_object(poll->question_, true, -1), std::move(poll_options), total_voter_count, + std::move(recent_voters), poll->is_anonymous_, std::move(poll_type), open_period, close_date, poll->is_closed_); } telegram_api::object_ptr PollManager::get_input_poll_option(const PollOption &poll_option) { return telegram_api::make_object( - telegram_api::make_object(poll_option.text_, Auto()), + get_input_text_with_entities(nullptr, poll_option.text_, "get_input_poll_option"), BufferSlice(poll_option.data_)); } -PollId PollManager::create_poll(string &&question, vector &&options, bool is_anonymous, +PollId PollManager::create_poll(FormattedText &&question, vector &&options, bool is_anonymous, bool allow_multiple_answers, bool is_quiz, int32 correct_option_id, FormattedText &&explanation, int32 open_period, int32 close_date, bool is_closed) { + remove_unallowed_entities(question); + for (auto &option : options) { + remove_unallowed_entities(option); + } auto poll = make_unique(); poll->question_ = std::move(question); int pos = '0'; @@ -794,10 +802,10 @@ string PollManager::get_poll_search_text(PollId poll_id) const { auto poll = get_poll(poll_id); CHECK(poll != nullptr); - string result = poll->question_; + string result = poll->question_.text; for (auto &option : poll->options_) { result += ' '; - result += option.text_; + result += option.text_.text; } return result; } @@ -1551,7 +1559,7 @@ tl_object_ptr PollManager::get_input_media(PollId poll flags, telegram_api::make_object( 0, poll_flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, - telegram_api::make_object(poll->question_, Auto()), + get_input_text_with_entities(nullptr, poll->question_, "get_input_media_poll"), transform(poll->options_, get_input_poll_option), poll->open_period_, poll->close_date_), std::move(correct_answers), poll->explanation_.text, get_input_message_entities(td_->user_manager_.get(), poll->explanation_.entities, "get_input_media_poll")); @@ -1561,7 +1569,8 @@ vector PollManager::get_poll_options( vector> &&poll_options) { return transform(std::move(poll_options), [](telegram_api::object_ptr &&poll_option) { PollOption option; - option.text_ = std::move(poll_option->text_->text_); + option.text_ = get_formatted_text(nullptr, std::move(poll_option->text_), false, true, true, "get_poll_options"); + remove_unallowed_entities(option.text_); option.data_ = poll_option->option_.as_slice().str(); return option; }); @@ -1638,13 +1647,14 @@ PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptroptions_ = get_poll_options(std::move(poll_server->answers_)); are_options_changed = true; } else { - for (size_t i = 0; i < poll->options_.size(); i++) { - if (poll->options_[i].text_ != poll_server->answers_[i]->text_->text_) { - poll->options_[i].text_ = std::move(poll_server->answers_[i]->text_->text_); + auto options = get_poll_options(std::move(poll_server->answers_)); + for (size_t i = 0; i < options.size(); i++) { + if (poll->options_[i].text_ != options[i].text_) { + poll->options_[i].text_ = std::move(options[i].text_); is_changed = true; } - if (poll->options_[i].data_ != poll_server->answers_[i]->option_.as_slice()) { - poll->options_[i].data_ = poll_server->answers_[i]->option_.as_slice().str(); + if (poll->options_[i].data_ != options[i].data_) { + poll->options_[i].data_ = std::move(options[i].data_); poll->options_[i].voter_count_ = 0; poll->options_[i].is_chosen_ = false; are_options_changed = true; @@ -1670,8 +1680,10 @@ PollId PollManager::on_get_poll(PollId poll_id, tl_object_ptrquestion_ != poll_server->question_->text_) { - poll->question_ = std::move(poll_server->question_->text_); + auto question = get_formatted_text(nullptr, std::move(poll_server->question_), false, true, true, "on_get_poll"); + remove_unallowed_entities(question); + if (poll->question_ != question) { + poll->question_ = std::move(question); is_changed = true; } poll_server_is_closed = (poll_server->flags_ & telegram_api::poll::CLOSED_MASK) != 0; diff --git a/td/telegram/PollManager.h b/td/telegram/PollManager.h index d690d9ed2..3edaef2ab 100644 --- a/td/telegram/PollManager.h +++ b/td/telegram/PollManager.h @@ -49,9 +49,9 @@ class PollManager final : public Actor { static bool is_local_poll_id(PollId poll_id); - PollId create_poll(string &&question, vector &&options, bool is_anonymous, bool allow_multiple_answers, - bool is_quiz, int32 correct_option_id, FormattedText &&explanation, int32 open_period, - int32 close_date, bool is_closed); + PollId create_poll(FormattedText &&question, vector &&options, bool is_anonymous, + bool allow_multiple_answers, bool is_quiz, int32 correct_option_id, FormattedText &&explanation, + int32 open_period, int32 close_date, bool is_closed); void register_poll(PollId poll_id, MessageFullId message_full_id, const char *source); @@ -103,7 +103,7 @@ class PollManager final : public Actor { private: struct PollOption { - string text_; + FormattedText text_; string data_; int32 voter_count_ = 0; bool is_chosen_ = false; @@ -115,7 +115,7 @@ class PollManager final : public Actor { }; struct Poll { - string question_; + FormattedText question_; vector options_; vector recent_voter_dialog_ids_; vector> recent_voter_min_channels_; @@ -159,6 +159,8 @@ class PollManager final : public Actor { static void on_unload_poll_timeout_callback(void *poll_manager_ptr, int64 poll_id_int); + static void remove_unallowed_entities(FormattedText &text); + static td_api::object_ptr get_poll_option_object(const PollOption &poll_option); static telegram_api::object_ptr get_input_poll_option(const PollOption &poll_option); diff --git a/td/telegram/PollManager.hpp b/td/telegram/PollManager.hpp index 03ed5d64e..9ccd8e95a 100644 --- a/td/telegram/PollManager.hpp +++ b/td/telegram/PollManager.hpp @@ -20,25 +20,35 @@ namespace td { template void PollManager::PollOption::store(StorerT &storer) const { using ::td::store; + bool has_entities = !text_.entities.empty(); BEGIN_STORE_FLAGS(); STORE_FLAG(is_chosen_); + STORE_FLAG(has_entities); END_STORE_FLAGS(); - store(text_, storer); + store(text_.text, storer); store(data_, storer); store(voter_count_, storer); + if (has_entities) { + store(text_.entities, storer); + } } template void PollManager::PollOption::parse(ParserT &parser) { using ::td::parse; + bool has_entities; BEGIN_PARSE_FLAGS(); PARSE_FLAG(is_chosen_); + PARSE_FLAG(has_entities); END_PARSE_FLAGS(); - parse(text_, parser); + parse(text_.text, parser); parse(data_, parser); parse(voter_count_, parser); + if (has_entities) { + parse(text_.entities, parser); + } } template @@ -50,6 +60,7 @@ void PollManager::Poll::store(StorerT &storer) const { bool has_explanation = !explanation_.text.empty(); bool has_recent_voter_dialog_ids = !recent_voter_dialog_ids_.empty(); bool has_recent_voter_min_channels = !recent_voter_min_channels_.empty(); + bool has_question_entities = !question_.entities.empty(); BEGIN_STORE_FLAGS(); STORE_FLAG(is_closed_); STORE_FLAG(is_public); @@ -62,9 +73,10 @@ void PollManager::Poll::store(StorerT &storer) const { STORE_FLAG(is_updated_after_close_); STORE_FLAG(has_recent_voter_dialog_ids); STORE_FLAG(has_recent_voter_min_channels); + STORE_FLAG(has_question_entities); END_STORE_FLAGS(); - store(question_, storer); + store(question_.text, storer); store(options_, storer); store(total_voter_count_, storer); if (is_quiz_) { @@ -85,6 +97,9 @@ void PollManager::Poll::store(StorerT &storer) const { if (has_recent_voter_min_channels) { store(recent_voter_min_channels_, storer); } + if (has_question_entities) { + store(question_.entities, storer); + } } template @@ -97,6 +112,7 @@ void PollManager::Poll::parse(ParserT &parser) { bool has_explanation; bool has_recent_voter_dialog_ids; bool has_recent_voter_min_channels; + bool has_question_entities; BEGIN_PARSE_FLAGS(); PARSE_FLAG(is_closed_); PARSE_FLAG(is_public); @@ -109,10 +125,11 @@ void PollManager::Poll::parse(ParserT &parser) { PARSE_FLAG(is_updated_after_close_); PARSE_FLAG(has_recent_voter_dialog_ids); PARSE_FLAG(has_recent_voter_min_channels); + PARSE_FLAG(has_question_entities); END_PARSE_FLAGS(); is_anonymous_ = !is_public; - parse(question_, parser); + parse(question_.text, parser); parse(options_, parser); parse(total_voter_count_, parser); if (is_quiz_) { @@ -141,6 +158,9 @@ void PollManager::Poll::parse(ParserT &parser) { if (has_recent_voter_min_channels) { parse(recent_voter_min_channels_, parser); } + if (has_question_entities) { + parse(question_.entities, parser); + } } template @@ -152,6 +172,9 @@ void PollManager::store_poll(PollId poll_id, StorerT &storer) const { bool has_open_period = poll->open_period_ != 0; bool has_close_date = poll->close_date_ != 0; bool has_explanation = !poll->explanation_.text.empty(); + bool has_question_entities = !poll->question_.entities.empty(); + bool has_option_entities = + td::any_of(poll->options_, [](const PollOption &option) { return !option.text_.entities.empty(); }); BEGIN_STORE_FLAGS(); STORE_FLAG(poll->is_closed_); STORE_FLAG(poll->is_anonymous_); @@ -160,9 +183,11 @@ void PollManager::store_poll(PollId poll_id, StorerT &storer) const { STORE_FLAG(has_open_period); STORE_FLAG(has_close_date); STORE_FLAG(has_explanation); + STORE_FLAG(has_question_entities); + STORE_FLAG(has_option_entities); END_STORE_FLAGS(); - store(poll->question_, storer); - vector options = transform(poll->options_, [](const PollOption &option) { return option.text_; }); + store(poll->question_.text, storer); + vector options = transform(poll->options_, [](const PollOption &option) { return option.text_.text; }); store(options, storer); if (poll->is_quiz_) { store(poll->correct_option_id_, storer); @@ -176,6 +201,13 @@ void PollManager::store_poll(PollId poll_id, StorerT &storer) const { if (has_explanation) { store(poll->explanation_, storer); } + if (has_question_entities) { + store(poll->question_.entities, storer); + } + if (has_option_entities) { + auto option_entities = transform(poll->options_, [](const PollOption &option) { return option.text_.entities; }); + store(option_entities, storer); + } } } @@ -185,8 +217,8 @@ PollId PollManager::parse_poll(ParserT &parser) { td::parse(poll_id_int, parser); PollId poll_id(poll_id_int); if (is_local_poll_id(poll_id)) { - string question; - vector options; + FormattedText question; + vector options; FormattedText explanation; int32 open_period = 0; int32 close_date = 0; @@ -197,6 +229,8 @@ PollId PollManager::parse_poll(ParserT &parser) { bool has_open_period = false; bool has_close_date = false; bool has_explanation = false; + bool has_question_entities = false; + bool has_option_entities = false; int32 correct_option_id = -1; if (parser.version() >= static_cast(Version::SupportPolls2_0)) { @@ -208,10 +242,13 @@ PollId PollManager::parse_poll(ParserT &parser) { PARSE_FLAG(has_open_period); PARSE_FLAG(has_close_date); PARSE_FLAG(has_explanation); + PARSE_FLAG(has_question_entities); + PARSE_FLAG(has_option_entities); END_PARSE_FLAGS(); } - parse(question, parser); - parse(options, parser); + parse(question.text, parser); + vector option_texts; + parse(option_texts, parser); if (is_quiz) { parse(correct_option_id, parser); if (correct_option_id < -1 || correct_option_id >= static_cast(options.size())) { @@ -227,6 +264,19 @@ PollId PollManager::parse_poll(ParserT &parser) { if (has_explanation) { parse(explanation, parser); } + if (has_question_entities) { + parse(question.entities, parser); + } + vector> option_entities; + if (has_option_entities) { + parse(option_entities, parser); + CHECK(option_entities.size() == option_texts.size()); + } else { + option_entities.resize(option_texts.size()); + } + for (size_t i = 0; i < option_texts.size(); i++) { + options.push_back({std::move(option_texts[i]), std::move(option_entities[i])}); + } if (parser.get_error() != nullptr) { return PollId(); } diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index dbe0b8295..243ab75f0 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -1728,7 +1728,7 @@ class CliClient final : public Actor { static td_api::object_ptr as_formatted_text( const string &text, vector> entities = {}) { if (entities.empty() && !text.empty()) { - Slice unused_reserved_characters("#+-={}.!"); + Slice unused_reserved_characters("#+-={}."); string new_text; for (size_t i = 0; i < text.size(); i++) { auto c = text[i]; @@ -5301,7 +5301,8 @@ class CliClient final : public Actor { ChatId chat_id; string question; get_args(args, chat_id, question, args); - auto options = autosplit_str(args); + vector> options = + transform(autosplit_str(args), [](const string &option) { return as_formatted_text(option); }); td_api::object_ptr poll_type; if (op == "squiz") { poll_type = td_api::make_object(narrow_cast(options.size() - 1), @@ -5309,8 +5310,9 @@ class CliClient final : public Actor { } else { poll_type = td_api::make_object(op == "spollm"); } - send_message(chat_id, td_api::make_object(question, std::move(options), op != "spollp", - std::move(poll_type), 0, 0, false)); + send_message(chat_id, + td_api::make_object(as_formatted_text(question), std::move(options), + op != "spollp", std::move(poll_type), 0, 0, false)); } else if (op == "sp") { ChatId chat_id; string photo;