diff --git a/include/tgbot/TgTypeParser.h b/include/tgbot/TgTypeParser.h index 661930e0..2348d446 100644 --- a/include/tgbot/TgTypeParser.h +++ b/include/tgbot/TgTypeParser.h @@ -957,6 +957,10 @@ class TGBOT_API TgTypeParser { } void appendToJson(std::string& json, const std::string& varName, const std::string& value) const; + + /// Appends `"varName":["escapedValue1","escapedValue2",...],` to `json`. + /// Does nothing when `values` is empty. Each element is JSON-escaped. + void appendStringArrayToJson(std::string& json, const std::string& varName, const std::vector& values) const; }; } diff --git a/src/Api.cpp b/src/Api.cpp index 9c208303..ee4578e3 100644 --- a/src/Api.cpp +++ b/src/Api.cpp @@ -59,7 +59,7 @@ bool Api::setWebhook(const std::string& url, if (allowedUpdates != nullptr) { auto allowedUpdatesJson = _tgTypeParser.parseArray( [] (const std::string& s)->std::string { - return s; + return "\"" + StringTools::escapeJsonString(s) + "\""; }, *allowedUpdates); args.emplace_back("allowed_updates", allowedUpdatesJson); } diff --git a/src/TgTypeParser.cpp b/src/TgTypeParser.cpp index 5ccab614..27061ab6 100644 --- a/src/TgTypeParser.cpp +++ b/src/TgTypeParser.cpp @@ -1,5 +1,7 @@ #include "tgbot/TgTypeParser.h" +#include "tgbot/tools/StringTools.h" + namespace TgBot { Update::Ptr TgTypeParser::parseJsonAndGetUpdate(const boost::property_tree::ptree& data) const { @@ -97,10 +99,7 @@ std::string TgTypeParser::parseWebhookInfo(const WebhookInfo::Ptr& object) const appendToJson(result, "last_error_message", object->lastErrorMessage); appendToJson(result, "last_synchronization_error_date", object->lastSynchronizationErrorDate); appendToJson(result, "max_connections", object->maxConnections); - appendToJson(result, "allowed_updates", parseArray( - [] (const std::string& s)->std::string { - return s; - }, object->allowedUpdates)); + appendStringArrayToJson(result, "allowed_updates", object->allowedUpdates); removeLastComma(result); result += '}'; return result; @@ -227,10 +226,7 @@ std::string TgTypeParser::parseChat(const Chat::Ptr& object) const { appendToJson(result, "last_name", object->lastName); appendToJson(result, "is_forum", object->isForum); appendToJson(result, "photo", parseChatPhoto(object->photo)); - appendToJson(result, "active_usernames", parseArray( - [] (const std::string& s)->std::string { - return s; - }, object->activeUsernames)); + appendStringArrayToJson(result, "active_usernames", object->activeUsernames); appendToJson(result, "birthdate", parseBirthdate(object->birthdate)); appendToJson(result, "business_intro", parseBusinessIntro(object->businessIntro)); appendToJson(result, "business_location", parseBusinessLocation(object->businessLocation)); @@ -1650,10 +1646,7 @@ std::string TgTypeParser::parseGiveaway(const Giveaway::Ptr& object) const { appendToJson(result, "only_new_members", object->onlyNewMembers); appendToJson(result, "has_public_winners", object->hasPublicWinners); appendToJson(result, "prize_description", object->prizeDescription); - appendToJson(result, "country_codes", parseArray( - [] (const std::string& s)->std::string { - return s; - }, object->countryCodes)); + appendStringArrayToJson(result, "country_codes", object->countryCodes); appendToJson(result, "premium_subscription_month_count", object->premiumSubscriptionMonthCount); removeLastComma(result); result += '}'; @@ -3819,15 +3812,9 @@ std::string TgTypeParser::parseInputSticker(const InputSticker::Ptr& object) con result += '{'; appendToJson(result, "sticker", object->sticker); appendToJson(result, "format", object->format); - appendToJson(result, "emoji_list", parseArray( - [] (const std::string& s)->std::string { - return s; - }, object->emojiList)); + appendStringArrayToJson(result, "emoji_list", object->emojiList); appendToJson(result, "mask_position", parseMaskPosition(object->maskPosition)); - appendToJson(result, "keywords", parseArray( - [] (const std::string& s)->std::string { - return s; - }, object->keywords)); + appendStringArrayToJson(result, "keywords", object->keywords); removeLastComma(result); result += '}'; return result; @@ -5416,10 +5403,7 @@ std::string TgTypeParser::parsePassportElementErrorFiles(const PassportElementEr // This function will be called by parsePassportElementError(), so I don't add // curly brackets to the result std::string. std::string result; - appendToJson(result, "file_hashes", - parseArray([] (const std::string& s)->std::string { - return s; - }, object->fileHashes)); + appendStringArrayToJson(result, "file_hashes", object->fileHashes); // The last comma will be erased by parsePassportElementError(). return result; } @@ -5460,10 +5444,7 @@ std::string TgTypeParser::parsePassportElementErrorTranslationFiles(const Passpo // This function will be called by parsePassportElementError(), so I don't add // curly brackets to the result std::string. std::string result; - appendToJson(result, "file_hashes", - parseArray([] (const std::string& s)->std::string { - return s; - }, object->fileHashes)); + appendStringArrayToJson(result, "file_hashes", object->fileHashes); // The last comma will be erased by parsePassportElementError(). return result; } @@ -5582,6 +5563,24 @@ std::string TgTypeParser::parseGenericReply(const GenericReply::Ptr& object) con return ""; } +void TgTypeParser::appendStringArrayToJson(std::string& json, const std::string& varName, const std::vector& values) const { + if (values.empty()) { + return; + } + json += '"'; + json += varName; + json += R"(":[)"; + for (std::size_t i = 0; i < values.size(); ++i) { + if (i != 0) { + json += ','; + } + json += '"'; + json += StringTools::escapeJsonString(values[i]); + json += '"'; + } + json += "],"; +} + void TgTypeParser::appendToJson(std::string& json, const std::string& varName, const std::string& value) const { if (value.empty()) { return; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 899035f8..5af0a3eb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,7 @@ set(TEST_SRC_LIST main.cpp tgbot/Api.cpp + tgbot/TgTypeParser.cpp tgbot/net/Url.cpp tgbot/net/HttpParser.cpp tgbot/tools/StringTools.cpp diff --git a/test/tgbot/TgTypeParser.cpp b/test/tgbot/TgTypeParser.cpp new file mode 100644 index 00000000..62df0da7 --- /dev/null +++ b/test/tgbot/TgTypeParser.cpp @@ -0,0 +1,61 @@ +#include +#include +#include + +#include + +#include "tgbot/TgTypeParser.h" +#include "tgbot/types/InputSticker.h" + +using namespace std; +using namespace TgBot; + +BOOST_AUTO_TEST_SUITE(tTgTypeParser) + +// Regression tests for https://github.com/reo7sp/tgbot-cpp/issues/346 — +// string arrays were emitted as `[a,b]` (unquoted identifiers) instead of +// `["a","b"]`, producing invalid JSON and breaking sticker creation. + +BOOST_AUTO_TEST_CASE(parseInputStickerEmojiListIsJsonArray) { + TgTypeParser parser; + auto sticker = make_shared(); + sticker->sticker = "file_id_abc"; + sticker->format = "static"; + sticker->emojiList = {"smile", "heart"}; + + const string json = parser.parseInputSticker(sticker); + + BOOST_CHECK(json.find(R"("emoji_list":["smile","heart"])") != string::npos); + // The pre-fix output emitted the array as `[smile,heart]` — assert no regression. + BOOST_CHECK(json.find("[smile,heart]") == string::npos); +} + +BOOST_AUTO_TEST_CASE(parseInputStickerEscapesJsonSpecialsInEmoji) { + TgTypeParser parser; + auto sticker = make_shared(); + sticker->sticker = "id"; + sticker->format = "static"; + sticker->emojiList = {R"(a"b)", R"(c\d)"}; + + const string json = parser.parseInputSticker(sticker); + + BOOST_CHECK(json.find(R"("a\"b")") != string::npos); + BOOST_CHECK(json.find(R"("c\\d")") != string::npos); +} + +BOOST_AUTO_TEST_CASE(parseInputStickerOmitsEmptyKeywords) { + // Empty arrays should not appear in the output at all, matching the + // behaviour of the previous `appendToJson(... parseArray(...))` pattern. + TgTypeParser parser; + auto sticker = make_shared(); + sticker->sticker = "id"; + sticker->format = "static"; + sticker->emojiList = {"a"}; + // keywords is default-constructed empty + + const string json = parser.parseInputSticker(sticker); + + BOOST_CHECK(json.find("keywords") == string::npos); +} + +BOOST_AUTO_TEST_SUITE_END()