From 879a7981cdb1b61b8ec41c774ee1e9d41d42f18f Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 17 Nov 2025 22:15:53 +0300 Subject: [PATCH 01/33] Init commit of bot development --- modules/bot/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/bot/README.md diff --git a/modules/bot/README.md b/modules/bot/README.md new file mode 100644 index 0000000..90c4d4b --- /dev/null +++ b/modules/bot/README.md @@ -0,0 +1 @@ +### Здесь будет часть, отвечающая за телеграм ботика From 602e9b62d85e627a4888bec4f456c1320c5f123d Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 18 Nov 2025 17:30:43 +0300 Subject: [PATCH 02/33] Started creating structure of bot interface --- modules/bot/front/.gitignore | 1 + modules/bot/front/CMakeLists.txt | 23 ++++++++++++++++ modules/bot/front/include/KeyboardFactory.hpp | 7 +++++ modules/bot/front/include/constants.hpp | 14 ++++++++++ modules/bot/front/include/front.hpp | 26 +++++++++++++++++++ modules/bot/front/src/KeyboardFactory.cpp | 15 +++++++++++ modules/bot/front/src/front.cpp | 21 +++++++++++++++ modules/bot/front/src/main.cpp | 15 +++++++++++ 8 files changed, 122 insertions(+) create mode 100644 modules/bot/front/.gitignore create mode 100644 modules/bot/front/CMakeLists.txt create mode 100644 modules/bot/front/include/KeyboardFactory.hpp create mode 100644 modules/bot/front/include/constants.hpp create mode 100644 modules/bot/front/include/front.hpp create mode 100644 modules/bot/front/src/KeyboardFactory.cpp create mode 100644 modules/bot/front/src/front.cpp create mode 100644 modules/bot/front/src/main.cpp diff --git a/modules/bot/front/.gitignore b/modules/bot/front/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/modules/bot/front/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/modules/bot/front/CMakeLists.txt b/modules/bot/front/CMakeLists.txt new file mode 100644 index 0000000..206c47f --- /dev/null +++ b/modules/bot/front/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.10.2) +project(AnimeBot) + +file(GLOB SOURCES "src/*.cpp") +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") +set(Boost_USE_MULTITHREADED ON) + +find_package(Threads REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(Boost COMPONENTS system REQUIRED) +find_package(CURL) +include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR}) +include_directories(include/) +if (CURL_FOUND) + include_directories(${CURL_INCLUDE_DIRS}) + add_definitions(-DHAVE_CURL) +endif() + +add_executable(AnimeBot ${SOURCES}) + +target_link_libraries(AnimeBot /usr/local/lib/libTgBot.a ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES} ${Boost_LIBRARIES} ${CURL_LIBRARIES}) diff --git a/modules/bot/front/include/KeyboardFactory.hpp b/modules/bot/front/include/KeyboardFactory.hpp new file mode 100644 index 0000000..9e15982 --- /dev/null +++ b/modules/bot/front/include/KeyboardFactory.hpp @@ -0,0 +1,7 @@ +#include + +class KeyboardFactory { +public: + /// Create keyboard for main menu + static TgBot::InlineKeyboardMarkup::Ptr createMainMenu(); +}; diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp new file mode 100644 index 0000000..212a88f --- /dev/null +++ b/modules/bot/front/include/constants.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace BotConstants { + namespace Button { + const std::string FIND_ANIME = "Найти аниме"; + const std::string MY_TITLES = "Мои тайтлы"; + } + namespace Callback { + const std::string FIND_ANIME = "action:find_anime"; + const std::string MY_TITLES = "navigation:my_titles"; + } +} diff --git a/modules/bot/front/include/front.hpp b/modules/bot/front/include/front.hpp new file mode 100644 index 0000000..f2c6485 --- /dev/null +++ b/modules/bot/front/include/front.hpp @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include + +#include + +class AnimeBot { +private: + std::string token; + TgBot::Bot bot; + +public: + /// Init Bot + AnimeBot(const std::string& token); + + /// Main menu + void setupHandlers(); + + /// Get TgBot::Bot + TgBot::Bot& getBot(); + + /// Function creates main menu and sends it to user with chatId + void sendMainMenu(int64_t chatId); +}; diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp new file mode 100644 index 0000000..1bd2cab --- /dev/null +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -0,0 +1,15 @@ +#include "KeyboardFactory.hpp" +#include "constants.hpp" + +TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() { + auto keyboard = std::make_shared(); + TgBot::InlineKeyboardButton::Ptr button1(new TgBot::InlineKeyboardButton); + button1->text = BotConstants::Button::FIND_ANIME; + button1->callbackData = BotConstants::Callback::FIND_ANIME; + TgBot::InlineKeyboardButton::Ptr button2(new TgBot::InlineKeyboardButton); + button2->text = BotConstants::Button::MY_TITLES; + button2->callbackData = BotConstants::Callback::MY_TITLES; + + keyboard->inlineKeyboard = {{button1, button2}}; + return keyboard; +} diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp new file mode 100644 index 0000000..d6a0b88 --- /dev/null +++ b/modules/bot/front/src/front.cpp @@ -0,0 +1,21 @@ +#include "front.hpp" +#include "KeyboardFactory.hpp" + +AnimeBot::AnimeBot(const std::string& token) : bot(token) { + setupHandlers(); +} + +void AnimeBot::setupHandlers() { + bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { + sendMainMenu(message->chat->id); + }); +} + +void AnimeBot::sendMainMenu(int64_t chatId) { + auto keyboard = KeyboardFactory::createMainMenu(); + bot.getApi().sendMessage(chatId, "Главное меню", nullptr, nullptr, keyboard); +} + +TgBot::Bot& AnimeBot::getBot() { + return bot; +} diff --git a/modules/bot/front/src/main.cpp b/modules/bot/front/src/main.cpp new file mode 100644 index 0000000..9632360 --- /dev/null +++ b/modules/bot/front/src/main.cpp @@ -0,0 +1,15 @@ +#include + +int main() { + AnimeBot bot(getenv("TOKEN")); + TgBot::TgLongPoll longPoll(bot.getBot()); + + while (true) { + try { + longPoll.start(); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + } + return 0; +} From 45ce5da0accff30f7d88de0acf63ec4545fc4f8e Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 18 Nov 2025 17:44:03 +0300 Subject: [PATCH 03/33] Changed main menu text --- modules/bot/front/include/constants.hpp | 3 +++ modules/bot/front/src/front.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 212a88f..0f24cae 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -11,4 +11,7 @@ namespace BotConstants { const std::string FIND_ANIME = "action:find_anime"; const std::string MY_TITLES = "navigation:my_titles"; } + namespace Text { + const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; + } } diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index d6a0b88..8294cfe 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -1,5 +1,6 @@ #include "front.hpp" #include "KeyboardFactory.hpp" +#include "constants.hpp" AnimeBot::AnimeBot(const std::string& token) : bot(token) { setupHandlers(); @@ -13,7 +14,7 @@ void AnimeBot::setupHandlers() { void AnimeBot::sendMainMenu(int64_t chatId) { auto keyboard = KeyboardFactory::createMainMenu(); - bot.getApi().sendMessage(chatId, "Главное меню", nullptr, nullptr, keyboard); + bot.getApi().sendMessage(chatId, BotConstants::Text::MAIN_MENU, nullptr, nullptr, keyboard); } TgBot::Bot& AnimeBot::getBot() { From 45a1df4cbbc1067e83de6a5dbe22ab69694761d9 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 25 Nov 2025 19:45:44 +0300 Subject: [PATCH 04/33] Added MyTitles page passing --- modules/bot/front/CMakeLists.txt | 3 +- modules/bot/front/include/KeyboardFactory.hpp | 4 ++ modules/bot/front/include/constants.hpp | 4 ++ modules/bot/front/include/handlers.hpp | 13 ++++++ modules/bot/front/include/structs.hpp | 8 ++++ modules/bot/front/src/KeyboardFactory.cpp | 43 +++++++++++++++++++ modules/bot/front/src/front.cpp | 21 +++++++++ modules/bot/front/src/handlers.cpp | 15 +++++++ 8 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 modules/bot/front/include/handlers.hpp create mode 100644 modules/bot/front/include/structs.hpp create mode 100644 modules/bot/front/src/handlers.cpp diff --git a/modules/bot/front/CMakeLists.txt b/modules/bot/front/CMakeLists.txt index 206c47f..19ee60a 100644 --- a/modules/bot/front/CMakeLists.txt +++ b/modules/bot/front/CMakeLists.txt @@ -2,10 +2,11 @@ cmake_minimum_required(VERSION 3.10.2) project(AnimeBot) file(GLOB SOURCES "src/*.cpp") -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") set(Boost_USE_MULTITHREADED ON) +set(CMAKE_BUILD_TYPE Debug) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) diff --git a/modules/bot/front/include/KeyboardFactory.hpp b/modules/bot/front/include/KeyboardFactory.hpp index 9e15982..0d3d5f0 100644 --- a/modules/bot/front/include/KeyboardFactory.hpp +++ b/modules/bot/front/include/KeyboardFactory.hpp @@ -1,7 +1,11 @@ #include +#include "structs.hpp" class KeyboardFactory { public: /// Create keyboard for main menu static TgBot::InlineKeyboardMarkup::Ptr createMainMenu(); + + /// Create keyboard for My_Titles + static TgBot::InlineKeyboardMarkup::Ptr createMyTitles(std::vector titles); }; diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 0f24cae..4d3d5e9 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -6,10 +6,14 @@ namespace BotConstants { namespace Button { const std::string FIND_ANIME = "Найти аниме"; const std::string MY_TITLES = "Мои тайтлы"; + const std::string PREV = "<<Назад"; + const std::string NEXT = "Следующий>>"; } namespace Callback { const std::string FIND_ANIME = "action:find_anime"; const std::string MY_TITLES = "navigation:my_titles"; + const std::string LIST_PREV = ""; + const std::string LIST_NEXT = ""; } namespace Text { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp new file mode 100644 index 0000000..eb9d2ec --- /dev/null +++ b/modules/bot/front/include/handlers.hpp @@ -0,0 +1,13 @@ +#include <tgbot/tgbot.h> +#include <string> +#include <structs.hpp> + +struct HandlerResult { + std::string message; + TgBot::InlineKeyboardMarkup::Ptr keyboard; +}; + +class BotHandlers { +public: + static HandlerResult MyTitles(int64_t userId); +}; diff --git a/modules/bot/front/include/structs.hpp b/modules/bot/front/include/structs.hpp new file mode 100644 index 0000000..57f5e4a --- /dev/null +++ b/modules/bot/front/include/structs.hpp @@ -0,0 +1,8 @@ +#pragma once + +struct Title { + int64_t id; + std::string name; + std::string description; + int64_t num; +}; diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index 1bd2cab..bc1bdd0 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -13,3 +13,46 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() { keyboard->inlineKeyboard = {{button1, button2}}; return keyboard; } + +TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Title> titles) { + auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>(); + std::vector<TgBot::InlineKeyboardButton::Ptr> row; + std::vector<std::vector<TgBot::InlineKeyboardButton::Ptr>> layout; + + int counter = 0; + for(Title& title : titles) { + if(counter >= 6) { + break; + } + auto button = std::make_shared<TgBot::InlineKeyboardButton>(); + button->text = std::to_string(title.num) + " " + title.name; + button->callbackData = "title:" + std::to_string(title.num); + row.push_back(button); + counter++; + if(counter % 2 == 0) { + layout.push_back(row); + row.clear(); + } + } + + // TODO: Додумать логику, когда пришло 6 записей в конце + if(counter % 2 == 1) { + auto button = std::make_shared<TgBot::InlineKeyboardButton>(); + button->text = BotConstants::Button::PREV; + button->callbackData = BotConstants::Callback::LIST_PREV + std::to_string(titles[0].num); + layout[counter / 2].push_back(button); + } + else { + auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); + button_prev->text = BotConstants::Button::PREV; + button_prev->callbackData = BotConstants::Callback::LIST_PREV + std::to_string(titles[0].num); + auto button_next = std::make_shared<TgBot::InlineKeyboardButton>(); + button_next->text = BotConstants::Button::NEXT; + button_next->callbackData = BotConstants::Callback::LIST_NEXT + std::to_string(titles[5].num); + layout.push_back({button_prev, button_next}); + } + + keyboard->inlineKeyboard = layout; + + return keyboard; +} diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index 8294cfe..85fe2f5 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -1,6 +1,7 @@ #include "front.hpp" #include "KeyboardFactory.hpp" #include "constants.hpp" +#include "handlers.hpp" AnimeBot::AnimeBot(const std::string& token) : bot(token) { setupHandlers(); @@ -10,6 +11,20 @@ void AnimeBot::setupHandlers() { bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { sendMainMenu(message->chat->id); }); + + auto [text, kb] = BotHandlers::MyTitles(321); + + bot.getEvents().onCallbackQuery([text, kb, this](TgBot::CallbackQuery::Ptr query) { + bot.getApi().editMessageText( + text, + query->message->chat->id, + query->message->messageId, + "", + "", + nullptr, + kb + ); + }); } void AnimeBot::sendMainMenu(int64_t chatId) { @@ -20,3 +35,9 @@ void AnimeBot::sendMainMenu(int64_t chatId) { TgBot::Bot& AnimeBot::getBot() { return bot; } + +/* +void AnimeBot::showMyTitles() { + +} +*/ diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp new file mode 100644 index 0000000..0b887c1 --- /dev/null +++ b/modules/bot/front/src/handlers.cpp @@ -0,0 +1,15 @@ +#include "handlers.hpp" +#include "KeyboardFactory.hpp" +#include "structs.hpp" + +/// В угоду потокобезопасности создаем новый экземпляр TgBot::Api +HandlerResult BotHandlers::MyTitles(int64_t userId) { + // Здесь должен происходить запрос на сервер + std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; + + struct HandlerResult result; + result.keyboard = KeyboardFactory::createMyTitles(titles); + result.message = "1. Школа мертвяков\n2. KissXsis\n"; + + return result; +} From ea29fa79f005884f63241742ebbd391268d03f57 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Tue, 25 Nov 2025 22:05:12 +0300 Subject: [PATCH 05/33] getting ready to refactor the handlers structure --- modules/bot/front/include/constants.hpp | 20 +++++++++++++++----- modules/bot/front/include/handlers.hpp | 13 ++++++++++++- modules/bot/front/src/front.cpp | 7 ++++--- modules/bot/front/src/handlers.cpp | 15 ++++++++++++++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 4d3d5e9..595a369 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -7,13 +7,23 @@ namespace BotConstants { const std::string FIND_ANIME = "Найти аниме"; const std::string MY_TITLES = "Мои тайтлы"; const std::string PREV = "<<Назад"; - const std::string NEXT = "Следующий>>"; + const std::string NEXT = "Дальше>>"; } namespace Callback { - const std::string FIND_ANIME = "action:find_anime"; - const std::string MY_TITLES = "navigation:my_titles"; - const std::string LIST_PREV = ""; - const std::string LIST_NEXT = ""; + const std::string ACTION = "action:"; + const std::string FIND_ANIME = ACTION + "find_anime"; + const std::string ADD_REVIEW = ACTION + "add_review"; + const std::string ADD_STATUS = ACTION + "add_status"; + const std::string STATUS = "status:"; + const std::string WATCHING = STATUS + "watching"; + const std::string SEEN = STATUS + "seen"; + const std::string WANT = STATUS + "want"; + const std::string THROWN = STATUS + "thrown"; + const std::string NAVIGATION = "navigation:"; + const std::string MY_TITLES = NAVIGATION + "my_titles"; + const std::string LIST_PREV = NAVIGATION + "prev"; + const std::string LIST_NEXT = NAVIGATION + "next"; + const std::string CHOICE = "choice:"; } namespace Text { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index eb9d2ec..27aee3a 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -2,6 +2,7 @@ #include <string> #include <structs.hpp> +/// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения struct HandlerResult { std::string message; TgBot::InlineKeyboardMarkup::Ptr keyboard; @@ -9,5 +10,15 @@ struct HandlerResult { class BotHandlers { public: - static HandlerResult MyTitles(int64_t userId); + void handleCallback(const TgBot::CallbackQuery::Ptr query); + +private: + TgBot::Api botApi; + + void handleNavigation(const TgBot::CallbackQuery::Ptr query); + + /// @brief Получить очередную страницу тайтлов из списка пользователя + /// @param userId Идентификатор пользователя + /// @return HandlerResult + static HandlerResult returnMyTitles(int64_t userId); }; diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index 85fe2f5..e6d3034 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -12,10 +12,11 @@ void AnimeBot::setupHandlers() { sendMainMenu(message->chat->id); }); - auto [text, kb] = BotHandlers::MyTitles(321); + auto [text, kb] = BotHandlers::returnMyTitles(321); - bot.getEvents().onCallbackQuery([text, kb, this](TgBot::CallbackQuery::Ptr query) { - bot.getApi().editMessageText( + auto cp_api = bot.getApi(); + bot.getEvents().onCallbackQuery([text, kb, cp_api](TgBot::CallbackQuery::Ptr query) { + cp_api.editMessageText( text, query->message->chat->id, query->message->messageId, diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 0b887c1..cbd90da 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -1,9 +1,18 @@ #include "handlers.hpp" #include "KeyboardFactory.hpp" #include "structs.hpp" +#include "constants.hpp" + +void BotHandlers::handleCallback(const TgBot::CallbackQuery::Ptr query) { + std::string data = query -> data; + + if (data.starts_with(BotConstants::Callback::NAVIGATION)) { + handleNavigation(query); + } +} /// В угоду потокобезопасности создаем новый экземпляр TgBot::Api -HandlerResult BotHandlers::MyTitles(int64_t userId) { +HandlerResult BotHandlers::returnMyTitles(int64_t userId) { // Здесь должен происходить запрос на сервер std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; @@ -13,3 +22,7 @@ HandlerResult BotHandlers::MyTitles(int64_t userId) { return result; } + +void BotHandlers::handleNavigation(const TgBot::CallbackQuery::Ptr query) { + return; +} From cdc1aa2e6b7e34179207a90d7f22ee16ba6348b1 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 27 Nov 2025 16:24:29 +0300 Subject: [PATCH 06/33] Forming the BotHandlers class structure --- modules/bot/front/include/constants.hpp | 23 +++++----- modules/bot/front/include/front.hpp | 3 +- modules/bot/front/include/handlers.hpp | 22 ++++++++-- modules/bot/front/src/KeyboardFactory.cpp | 6 +-- modules/bot/front/src/front.cpp | 31 +++++-------- modules/bot/front/src/handlers.cpp | 53 ++++++++++++++++++++--- 6 files changed, 92 insertions(+), 46 deletions(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 595a369..75e691e 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -11,21 +11,22 @@ namespace BotConstants { } namespace Callback { const std::string ACTION = "action:"; - const std::string FIND_ANIME = ACTION + "find_anime"; - const std::string ADD_REVIEW = ACTION + "add_review"; - const std::string ADD_STATUS = ACTION + "add_status"; - const std::string STATUS = "status:"; - const std::string WATCHING = STATUS + "watching"; - const std::string SEEN = STATUS + "seen"; - const std::string WANT = STATUS + "want"; - const std::string THROWN = STATUS + "thrown"; + const std::string FIND_ANIME = ACTION + "find_anime"; + const std::string ADD_REVIEW = ACTION + "add_review"; + const std::string ADD_STATUS = ACTION + "add_status"; + const std::string STATUS = "status:"; + const std::string WATCHING = STATUS + "watching"; + const std::string SEEN = STATUS + "seen"; + const std::string WANT = STATUS + "want"; + const std::string THROWN = STATUS + "thrown"; const std::string NAVIGATION = "navigation:"; - const std::string MY_TITLES = NAVIGATION + "my_titles"; - const std::string LIST_PREV = NAVIGATION + "prev"; - const std::string LIST_NEXT = NAVIGATION + "next"; + const std::string MY_TITLES = NAVIGATION + "my_titles"; + const std::string LIST_PREV = NAVIGATION + "prev"; + const std::string LIST_NEXT = NAVIGATION + "next"; const std::string CHOICE = "choice:"; } namespace Text { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; + const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся"; } } diff --git a/modules/bot/front/include/front.hpp b/modules/bot/front/include/front.hpp index f2c6485..4d37ec8 100644 --- a/modules/bot/front/include/front.hpp +++ b/modules/bot/front/include/front.hpp @@ -5,11 +5,12 @@ #include <string> #include <tgbot/tgbot.h> +#include "handlers.hpp" class AnimeBot { private: - std::string token; TgBot::Bot bot; + BotHandlers handler; public: /// Init Bot diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 27aee3a..7440d00 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -1,3 +1,4 @@ +#pragma once #include <tgbot/tgbot.h> #include <string> #include <structs.hpp> @@ -9,13 +10,28 @@ struct HandlerResult { }; class BotHandlers { -public: - void handleCallback(const TgBot::CallbackQuery::Ptr query); +public: + BotHandlers(TgBot::Api api) : botApi(api) {;} + + /// @brief Обработка callback'ов из кнопок интерфейса + /// @param query запрос callback + void handleCallback(TgBot::CallbackQuery::Ptr query); + + /// @brief Обработка сообщений боту + /// @details + /// Функция для обработки сообщений, которые юзер отправляет + /// боту. Необходима для обработки ревью и названий искомого + /// аниме. Внутри себя проверяет текущий state пользователя + /// в боте. + /// @param message обрабатываемое сообщение + void handleMessage(TgBot::Message::Ptr message); private: TgBot::Api botApi; - void handleNavigation(const TgBot::CallbackQuery::Ptr query); + void handleNavigation(TgBot::CallbackQuery::Ptr query); + + void processCallbackImpl(TgBot::CallbackQuery::Ptr query); /// @brief Получить очередную страницу тайтлов из списка пользователя /// @param userId Идентификатор пользователя diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index bc1bdd0..fc1a3fd 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -39,16 +39,16 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Tit if(counter % 2 == 1) { auto button = std::make_shared<TgBot::InlineKeyboardButton>(); button->text = BotConstants::Button::PREV; - button->callbackData = BotConstants::Callback::LIST_PREV + std::to_string(titles[0].num); + button->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); layout[counter / 2].push_back(button); } else { auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); button_prev->text = BotConstants::Button::PREV; - button_prev->callbackData = BotConstants::Callback::LIST_PREV + std::to_string(titles[0].num); + button_prev->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); auto button_next = std::make_shared<TgBot::InlineKeyboardButton>(); button_next->text = BotConstants::Button::NEXT; - button_next->callbackData = BotConstants::Callback::LIST_NEXT + std::to_string(titles[5].num); + button_next->callbackData = BotConstants::Callback::LIST_NEXT + ':' + std::to_string(titles[5].num); layout.push_back({button_prev, button_next}); } diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index e6d3034..9ee010c 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -3,7 +3,9 @@ #include "constants.hpp" #include "handlers.hpp" -AnimeBot::AnimeBot(const std::string& token) : bot(token) { +AnimeBot::AnimeBot(const std::string& token) + : bot(token) + , handler(bot.getApi()) { setupHandlers(); } @@ -11,20 +13,13 @@ void AnimeBot::setupHandlers() { bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { sendMainMenu(message->chat->id); }); + + bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) { + handler.handleCallback(query); + }); - auto [text, kb] = BotHandlers::returnMyTitles(321); - - auto cp_api = bot.getApi(); - bot.getEvents().onCallbackQuery([text, kb, cp_api](TgBot::CallbackQuery::Ptr query) { - cp_api.editMessageText( - text, - query->message->chat->id, - query->message->messageId, - "", - "", - nullptr, - kb - ); + bot.getEvents().onAnyMessage([this](TgBot::Message::Ptr message) { + handler.handleMessage(message); }); } @@ -35,10 +30,4 @@ void AnimeBot::sendMainMenu(int64_t chatId) { TgBot::Bot& AnimeBot::getBot() { return bot; -} - -/* -void AnimeBot::showMyTitles() { - -} -*/ +} \ No newline at end of file diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index cbd90da..4ae1040 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -3,15 +3,23 @@ #include "structs.hpp" #include "constants.hpp" -void BotHandlers::handleCallback(const TgBot::CallbackQuery::Ptr query) { - std::string data = query -> data; - - if (data.starts_with(BotConstants::Callback::NAVIGATION)) { - handleNavigation(query); +void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) { + if (!query) { + // TODO: log + return; } + + try { + // TODO: Тут mutex на многопоточке + botApi.answerCallbackQuery(query->id); + } catch (const std::exception& e) { + std::cerr << "answerCallbackQuery error"; + //TODO: обработка ошибки + } + + processCallbackImpl(query); } -/// В угоду потокобезопасности создаем новый экземпляр TgBot::Api HandlerResult BotHandlers::returnMyTitles(int64_t userId) { // Здесь должен происходить запрос на сервер std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; @@ -23,6 +31,37 @@ HandlerResult BotHandlers::returnMyTitles(int64_t userId) { return result; } -void BotHandlers::handleNavigation(const TgBot::CallbackQuery::Ptr query) { +void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { + const std::string& data = query->data; + if (data == BotConstants::Callback::MY_TITLES) { + auto [text, kb] = BotHandlers::returnMyTitles(321); + botApi.editMessageText( + text, + query->message->chat->id, + query->message->messageId, + "", + "", + nullptr, + kb + ); + } + else { + botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); + } +} + +void BotHandlers::handleMessage(TgBot::Message::Ptr message) { + //TODO: просмотр состояния пользователя return; } + +void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { + const std::string& data = query->data; + + if (data.starts_with(BotConstants::Callback::NAVIGATION)) { + handleNavigation(query); + } + else { + botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); + } +} From 3d8abc3f0c20b8df3e2fb4db3f5b70056bd354d2 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 27 Nov 2025 16:24:46 +0300 Subject: [PATCH 07/33] Changed .gitignore --- modules/bot/front/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/bot/front/.gitignore b/modules/bot/front/.gitignore index 567609b..c315271 100644 --- a/modules/bot/front/.gitignore +++ b/modules/bot/front/.gitignore @@ -1 +1,4 @@ build/ +out/ +.vscode +api/generated-client \ No newline at end of file From 167e2323becdbf8f9ab2154bf5b50fa339526012 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 27 Nov 2025 17:33:04 +0300 Subject: [PATCH 08/33] Announce UserContext struct --- modules/bot/front/include/handlers.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 7440d00..313bee8 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -2,6 +2,7 @@ #include <tgbot/tgbot.h> #include <string> #include <structs.hpp> +#include <unordered_map> /// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения struct HandlerResult { @@ -9,6 +10,22 @@ struct HandlerResult { TgBot::InlineKeyboardMarkup::Ptr keyboard; }; +enum class UserState { + MAIN_MENU, // Главное меню + VIEWING_MY_TITLES, // Список моих тайтлов + AWAITING_TITLE_NAME, // Жду название тайтла для поиска + VIEWING_TITLE_PAGE, // Смотрю страничку тайтла + AWAITING_REVIEW, // Жду ревью на тайтл + VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл + VIEWING_REVIEW, // Смотрю (конкретное) ревью на тайтл + VIEWING_DESCRIPTION, // Смотрю описание тайтла +}; + +struct UserContext { + UserState state; + // Информация о тайтле +}; + class BotHandlers { public: BotHandlers(TgBot::Api api) : botApi(api) {;} @@ -28,6 +45,7 @@ public: private: TgBot::Api botApi; + std::unordered_map<int64_t, UserContext> userContexts; void handleNavigation(TgBot::CallbackQuery::Ptr query); From bd309d38c62b4c3f8822e68d3a27e1319e48fbc4 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 27 Nov 2025 17:34:04 +0300 Subject: [PATCH 09/33] Fixed .gitignore --- api/.gitignore | 1 + modules/bot/front/.gitignore | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 api/.gitignore diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..c492c4d --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +generated-client/ \ No newline at end of file diff --git a/modules/bot/front/.gitignore b/modules/bot/front/.gitignore index c315271..fd3551f 100644 --- a/modules/bot/front/.gitignore +++ b/modules/bot/front/.gitignore @@ -1,4 +1,2 @@ build/ -out/ -.vscode -api/generated-client \ No newline at end of file +out/ \ No newline at end of file From 7efd7bb6b03f1cb646fc8ed54f61d32c9fca6dcd Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 27 Nov 2025 18:13:40 +0300 Subject: [PATCH 10/33] Added cursor field to UserContext --- modules/bot/front/include/handlers.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 313bee8..b0c5545 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -22,8 +22,8 @@ enum class UserState { }; struct UserContext { - UserState state; - // Информация о тайтле + UserState state; // Текущее состояние пользователя + int64_t cursor; // Текущий курсор пользователя (id тайтла) }; class BotHandlers { From 12648e1a8fd449a7b6c984f1ebab3b29b8ae70ac Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 12:25:40 +0300 Subject: [PATCH 11/33] Changing the structure of the UserContext. Adding context history --- modules/bot/front/include/handlers.hpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index b0c5545..558bd19 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -19,11 +19,17 @@ enum class UserState { VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл VIEWING_REVIEW, // Смотрю (конкретное) ревью на тайтл VIEWING_DESCRIPTION, // Смотрю описание тайтла + ERROR, // Ошибка состояния }; +struct NavigationStep { + UserState state; + int64_t payload; // ID тайтла, ревью и т.д. +}; + + struct UserContext { - UserState state; // Текущее состояние пользователя - int64_t cursor; // Текущий курсор пользователя (id тайтла) + std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний }; class BotHandlers { From 28a7d9e69169dae5d1ac7f74f4eb1fd510448cd9 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 12:29:53 +0300 Subject: [PATCH 12/33] Added payload constant --- modules/bot/front/include/constants.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 75e691e..7a93931 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -3,6 +3,8 @@ #include <string> namespace BotConstants { + const int64_t NULL_PAYLOAD = -1; + namespace Button { const std::string FIND_ANIME = "Найти аниме"; const std::string MY_TITLES = "Мои тайтлы"; From e09b6658b297c8f23379382ea9a6f3af2986dc2f Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 12:30:34 +0300 Subject: [PATCH 13/33] Bad variant of navigation handler (not working) --- modules/bot/front/src/handlers.cpp | 99 +++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 4ae1040..977751e 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -32,22 +32,41 @@ HandlerResult BotHandlers::returnMyTitles(int64_t userId) { } void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { + int64_t userId = query->from->id; + auto it = userContexts.find(userId); + if (it == userContexts.end()) { + botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR); + return; + } + + UserContext& ctx = it->second; const std::string& data = query->data; - if (data == BotConstants::Callback::MY_TITLES) { - auto [text, kb] = BotHandlers::returnMyTitles(321); - botApi.editMessageText( - text, - query->message->chat->id, - query->message->messageId, - "", - "", - nullptr, - kb - ); - } - else { - botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); + + //HandlerResult response; + //UserContext newCtx; + + auto [response, newCtx] = newStateNavigation(query, ctx); + switch (ctx.state) { + case UserState::VIEWING_MY_TITLES: + response = BotHandlers::returnMyTitles(ctx.cursor); + break; + case UserState::VIEWING_REVIEW_LIST: + response = BotHandlers::returnReviewList(ctx.cursor); + break; + default: + botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR); + return; } + + botApi.editMessageText( + response.message, + query->message->chat->id, + query->message->messageId, + "", + "", + nullptr, + response.keyboard + ); } void BotHandlers::handleMessage(TgBot::Message::Ptr message) { @@ -65,3 +84,55 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); } } + +std::pair<HandlerResult, UserContext> BotHandlers::newStateNavigation(const TgBot::CallbackQuery::Ptr& query, const UserContext& ctx) { + const std::string& data = query->data; + switch (ctx.state) { + case UserState::MAIN_MENU: + if(data == BotConstants::Callback::MY_TITLES) { + UserContext newCtx{.state = UserState::VIEWING_MY_TITLES, .cursor = BotConstants::CURSOR_NOT_INIT}; + HandlerResult result = returnMyTitles(query->from->id); + + return {result, newCtx}; + } + case UserState::VIEWING_MY_TITLES: + if(data == BotConstants::Callback::LIST_PREV) { + + } + else if (data == BotConstants::Callback::LIST_NEXT) { + + } + case UserState::AWAITING_TITLE_NAME: + if(data == BotConstants::Callback::LIST_PREV) { + + } + case UserState::VIEWING_TITLE_PAGE: + if(data == BotConstants::Callback::LIST_PREV) { + + } + case UserState::AWAITING_REVIEW: + if(data == BotConstants::Callback::LIST_PREV) { + + } + case UserState::VIEWING_REVIEW_LIST: + if(data == BotConstants::Callback::LIST_PREV) { + + } + else if (data == BotConstants::Callback::LIST_NEXT) { + + } + case UserState::VIEWING_REVIEW: + if(data == BotConstants::Callback::LIST_PREV) { + + } + case UserState::VIEWING_DESCRIPTION: + if(data == BotConstants::Callback::LIST_PREV) { + + } + default: + UserContext newCtx{.state = UserState::ERROR, .cursor = BotConstants::CURSOR_NOT_INIT}; + HandlerResult result = {"", nullptr}; + + return {result, newCtx}; + } +} From a8dd448c954487fe73ebd552c8fc327396dbe1a1 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 12:42:11 +0300 Subject: [PATCH 14/33] Added a callback to go back through the state stack --- modules/bot/front/include/constants.hpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 7a93931..df7b534 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -18,13 +18,14 @@ namespace BotConstants { const std::string ADD_STATUS = ACTION + "add_status"; const std::string STATUS = "status:"; const std::string WATCHING = STATUS + "watching"; - const std::string SEEN = STATUS + "seen"; - const std::string WANT = STATUS + "want"; - const std::string THROWN = STATUS + "thrown"; + const std::string SEEN = STATUS + "seen"; + const std::string WANT = STATUS + "want"; + const std::string THROWN = STATUS + "thrown"; const std::string NAVIGATION = "navigation:"; const std::string MY_TITLES = NAVIGATION + "my_titles"; - const std::string LIST_PREV = NAVIGATION + "prev"; - const std::string LIST_NEXT = NAVIGATION + "next"; + const std::string LIST_PREV = NAVIGATION + "prev"; // Пагинация + const std::string LIST_NEXT = NAVIGATION + "next"; // Пагинация + const std::string NAV_BACK = NAVIGATION + "back"; // Возврат по стеку состояний const std::string CHOICE = "choice:"; } namespace Text { From b368ecc43b3b8a5b5f5e85c03e57f40f7ef1805e Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 12:57:07 +0300 Subject: [PATCH 15/33] Added context navigation logic --- modules/bot/front/include/handlers.hpp | 11 +++++++++++ modules/bot/front/src/handlers.cpp | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 558bd19..935de04 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -61,4 +61,15 @@ private: /// @param userId Идентификатор пользователя /// @return HandlerResult static HandlerResult returnMyTitles(int64_t userId); + + /// @brief Вход в новое состояние + /// @param ctx текущий контекст + /// @param newState новое состояние, добавляемое в стек + /// @param payload полезная нагрузка этого состояния + void pushState(UserContext& ctx, UserState newState, int64_t payload); + + /// @brief Возврат в предыдущее состояние + /// @param ctx Текущий контекст + /// @return true в случае успеха + bool popState(UserContext& ctx); }; diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 977751e..426fa6b 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -136,3 +136,13 @@ std::pair<HandlerResult, UserContext> BotHandlers::newStateNavigation(const TgBo return {result, newCtx}; } } + +void BotHandlers::pushState(UserContext& ctx, UserState newState, int64_t payload) { + ctx.history.push_back({newState, payload}); +} + +bool BotHandlers::popState(UserContext& ctx) { + if (ctx.history.size() <= 1) return false; // нельзя выйти из MAIN_MENU + ctx.history.pop_back(); + return true; +} From d69f5fcddf96dc54a564e0aa223ba6be7b75a6d8 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 15:18:31 +0300 Subject: [PATCH 16/33] fix(tgbot-front): start fixing navigation callback processing function --- modules/bot/front/src/handlers.cpp | 134 ++++++++++++++++++----------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 426fa6b..a3633eb 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -85,56 +85,66 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { } } -std::pair<HandlerResult, UserContext> BotHandlers::newStateNavigation(const TgBot::CallbackQuery::Ptr& query, const UserContext& ctx) { - const std::string& data = query->data; - switch (ctx.state) { - case UserState::MAIN_MENU: - if(data == BotConstants::Callback::MY_TITLES) { - UserContext newCtx{.state = UserState::VIEWING_MY_TITLES, .cursor = BotConstants::CURSOR_NOT_INIT}; - HandlerResult result = returnMyTitles(query->from->id); - - return {result, newCtx}; - } - case UserState::VIEWING_MY_TITLES: - if(data == BotConstants::Callback::LIST_PREV) { - - } - else if (data == BotConstants::Callback::LIST_NEXT) { - - } - case UserState::AWAITING_TITLE_NAME: - if(data == BotConstants::Callback::LIST_PREV) { - - } - case UserState::VIEWING_TITLE_PAGE: - if(data == BotConstants::Callback::LIST_PREV) { - - } - case UserState::AWAITING_REVIEW: - if(data == BotConstants::Callback::LIST_PREV) { - - } - case UserState::VIEWING_REVIEW_LIST: - if(data == BotConstants::Callback::LIST_PREV) { - - } - else if (data == BotConstants::Callback::LIST_NEXT) { - - } - case UserState::VIEWING_REVIEW: - if(data == BotConstants::Callback::LIST_PREV) { - - } - case UserState::VIEWING_DESCRIPTION: - if(data == BotConstants::Callback::LIST_PREV) { - - } - default: - UserContext newCtx{.state = UserState::ERROR, .cursor = BotConstants::CURSOR_NOT_INIT}; - HandlerResult result = {"", nullptr}; - - return {result, newCtx}; +void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { + int64_t userId = query->from->id; + auto it = userContexts.find(userId); + if (it == userContexts.end()) { + // TODO: log + std::cout << "Error: Не нашел пользователя " << userId; + return; } + + UserContext& ctx = it->second; + const auto& current = ctx.history.back(); // текущий экран + const std::string& data = query->data; + + // Пагинация (в списках) + if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT) + && (current.state == UserState::VIEWING_MY_TITLES || current.state == UserState::VIEWING_REVIEW_LIST || + current.state == UserState::VIEWING_FOUND_TITLES)) { + + int64_t newPayload = current.payload; + if (data == BotConstants::Callback::LIST_PREV && newPayload > 0) { + reducePayload(newPayload, current.state); + } else if (data == BotConstants::Callback::LIST_NEXT) { + increasePayload(newPayload, current.state); + } else { + if (data == BotConstants::Callback::LIST_PREV) { + std::cout << "Error: navigation:prev callback for 1st page" << std::endl; + return; + } + // TODO: log + std::cout << "Error: navigation:prev unknown error" << std::endl; + } + + ctx.history.back().payload = newPayload; + + auto result = renderCurrent(ctx); + editMessage(query, result); + return; + } + + // Обработка back по интерфейсу + if (data == BotConstants::Callback::NAV_BACK) { + if (!popState(ctx)) { + botApi.answerCallbackQuery(query->id, "Некуда возвращаться", true); + return; + } + auto result = renderCurrent(ctx); + editMessage(query, result); + return; + } + + // Переходы вперёд (push) + auto newStepOpt = computeNextStep(query, current); + if (!newStepOpt.has_value()) { + sendError(query->message->chat->id); + return; + } + + ctx.history.push_back(*newStepOpt); + auto result = renderCurrent(ctx); + editMessage(query, result); } void BotHandlers::pushState(UserContext& ctx, UserState newState, int64_t payload) { @@ -146,3 +156,29 @@ bool BotHandlers::popState(UserContext& ctx) { ctx.history.pop_back(); return true; } + +void BotHandlers::reducePayload(int64_t& payload, const UserState curState) { + if (curState == UserState::VIEWING_MY_TITLES || + curState == UserState::VIEWING_FOUND_TITLES) { + payload -= BotConstants::DISP_TITLES_NUM; + } else if (curState == UserState::VIEWING_REVIEW_LIST) { + payload -= BotConstants::DISP_REVIEW_NUM; + } else { + // TODO: log + payload = BotConstants::NULL_PAYLOAD; + std::cerr << "Error: reducePayload" << std::endl; + } +} + +void BotHandlers::increasePayload(int64_t& payload, const UserState curState) { + if (curState == UserState::VIEWING_MY_TITLES || + curState == UserState::VIEWING_FOUND_TITLES) { + payload += BotConstants::DISP_TITLES_NUM; + } else if (curState == UserState::VIEWING_REVIEW_LIST) { + payload += BotConstants::DISP_REVIEW_NUM; + } else { + // TODO: log + payload = BotConstants::NULL_PAYLOAD; + std::cerr << "Error: increasePayload" << std::endl; + } +} \ No newline at end of file From c815e96f4c11cd64c2afb737a2a000888a224ea5 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 15:21:07 +0300 Subject: [PATCH 17/33] feat(tgbot-front): add new funcs to work with payload --- modules/bot/front/include/handlers.hpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 935de04..c3753b6 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -14,6 +14,7 @@ enum class UserState { MAIN_MENU, // Главное меню VIEWING_MY_TITLES, // Список моих тайтлов AWAITING_TITLE_NAME, // Жду название тайтла для поиска + VIEWING_FOUND_TITLES, // Смотрю найденные тайтлы VIEWING_TITLE_PAGE, // Смотрю страничку тайтла AWAITING_REVIEW, // Жду ревью на тайтл VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл @@ -72,4 +73,14 @@ private: /// @param ctx Текущий контекст /// @return true в случае успеха bool popState(UserContext& ctx); + + /// @brief Уменьшает значение нагрузки с учетом текущего состояния + /// @param payload Изменяемое значение нагрузки + /// @param curState Текущее состояние + void reducePayload(int64_t& payload, const UserState curState); + + /// @brief Увеличивает значение нагрузки с учетом текущего состояния + /// @param payload Изменяемое значение нагрузки + /// @param curState Текущее состояние + void increasePayload(int64_t& payload, const UserState curState); }; From 0fdf5776124ac31dcabfe7d6a09853b478c598d7 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 28 Nov 2025 15:22:42 +0300 Subject: [PATCH 18/33] feat(tgbot-front): add consts for titles and revs number --- modules/bot/front/include/constants.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index df7b534..e384186 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -3,7 +3,9 @@ #include <string> namespace BotConstants { - const int64_t NULL_PAYLOAD = -1; + const int64_t NULL_PAYLOAD = -1; // Default value для payload + const int64_t DISP_TITLES_NUM = 6; // Количество тайтлов, отображаемых на страничке + const int64_t DISP_REVIEW_NUM = 4; // Количество ревью, отображаемых на страничке namespace Button { const std::string FIND_ANIME = "Найти аниме"; From ccf9722bb7729a87e26bcbf7d05ecfe2ec95d942 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Mon, 1 Dec 2025 23:28:23 +0300 Subject: [PATCH 19/33] feat(tgbot-front): implement the back button operation Add functions to handle navigation callback logic Also add function for creating initial user context (pay attention to auth and registration later) --- modules/bot/front/include/constants.hpp | 1 + modules/bot/front/include/handlers.hpp | 37 +++++- modules/bot/front/src/KeyboardFactory.cpp | 15 ++- modules/bot/front/src/front.cpp | 2 + modules/bot/front/src/handlers.cpp | 134 +++++++++++++++------- 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index e384186..8fee9d5 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -33,5 +33,6 @@ namespace BotConstants { namespace Text { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся"; + const std::string AUTH_ERROR = "Проблемы с авторизацией, попробуйте авторизоваться повторно"; } } diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index c3753b6..572984d 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -30,6 +30,7 @@ struct NavigationStep { struct UserContext { + int64_t userId; std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний }; @@ -50,6 +51,10 @@ public: /// @param message обрабатываемое сообщение void handleMessage(TgBot::Message::Ptr message); + /// @brief Создает контекст начального меню для пользователя + /// @param chatId id чата пользователя + void createInitContext(int64_t chatId); + private: TgBot::Api botApi; std::unordered_map<int64_t, UserContext> userContexts; @@ -60,8 +65,9 @@ private: /// @brief Получить очередную страницу тайтлов из списка пользователя /// @param userId Идентификатор пользователя + /// @param payload Полезная нагрузка /// @return HandlerResult - static HandlerResult returnMyTitles(int64_t userId); + static HandlerResult returnMyTitles(int64_t userId, int64_t payload); /// @brief Вход в новое состояние /// @param ctx текущий контекст @@ -83,4 +89,33 @@ private: /// @param payload Изменяемое значение нагрузки /// @param curState Текущее состояние void increasePayload(int64_t& payload, const UserState curState); + + /// @brief Редактирует текущее сообщение в диалоге с пользователем + /// @details Меняет текст сообщения и клавиатуру на те, что передаются + /// в аргументе response. Информацию об id чата и изменяемого сообщения + /// забирает из query, который возвращается с callback'ом после нажатия + /// кнопки в интерфейсе + /// @param query Callback запрос + /// @param response Параметры ответа: клавиатура и текст + void editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult response); + + /// @brief Отрисовка текущего экрана (соотв. контексту) + /// @param ctx - текущий контекст + /// @return HandlerResult для нового состояния сообщения + HandlerResult renderCurrent(const UserContext& ctx); + + /// @brief Логика переходов между контекстами (навигация на следующий шаг) + /// @param query - запрос + /// @param current - текущий шаг навигации + /// @return следующий NavigationStep при успехе (std::nullopt в случае ошибки) + std::optional<NavigationStep> computeNextStep(const TgBot::CallbackQuery::Ptr& query, + const NavigationStep& current); + + /// @brief Получить состояние страницы главного меню + /// @return HandlerResult с параметрами главного меню + HandlerResult showMainMenu(); + + /// @brief Посылает интерфейс обработки ошибки на callback запрос + /// @param query запрос + void sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText); }; diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index fc1a3fd..cf2500c 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -14,6 +14,7 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() { return keyboard; } +// TODO: Переписать с учетом констант на количество отображаемых тайтлов и нового callback'a TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Title> titles) { auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>(); std::vector<TgBot::InlineKeyboardButton::Ptr> row; @@ -39,13 +40,23 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Tit if(counter % 2 == 1) { auto button = std::make_shared<TgBot::InlineKeyboardButton>(); button->text = BotConstants::Button::PREV; - button->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); + if(titles[0].num == 1) { + button->callbackData = BotConstants::Callback::NAV_BACK; + } + else { + button->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); + } layout[counter / 2].push_back(button); } else { auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); button_prev->text = BotConstants::Button::PREV; - button_prev->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); + if(titles[0].num == 1) { + button_prev->callbackData = BotConstants::Callback::NAV_BACK; + } + else { + button_prev->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); + } auto button_next = std::make_shared<TgBot::InlineKeyboardButton>(); button_next->text = BotConstants::Button::NEXT; button_next->callbackData = BotConstants::Callback::LIST_NEXT + ':' + std::to_string(titles[5].num); diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index 9ee010c..4f463e1 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -12,6 +12,8 @@ AnimeBot::AnimeBot(const std::string& token) void AnimeBot::setupHandlers() { bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { sendMainMenu(message->chat->id); + //TODO: производить инициализацию контекста только после авторизации + handler.createInitContext(message->chat->id); }); bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) { diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index a3633eb..5bdef9d 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -20,7 +20,7 @@ void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) { processCallbackImpl(query); } -HandlerResult BotHandlers::returnMyTitles(int64_t userId) { +HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) { // Здесь должен происходить запрос на сервер std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; @@ -31,44 +31,6 @@ HandlerResult BotHandlers::returnMyTitles(int64_t userId) { return result; } -void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { - int64_t userId = query->from->id; - auto it = userContexts.find(userId); - if (it == userContexts.end()) { - botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR); - return; - } - - UserContext& ctx = it->second; - const std::string& data = query->data; - - //HandlerResult response; - //UserContext newCtx; - - auto [response, newCtx] = newStateNavigation(query, ctx); - switch (ctx.state) { - case UserState::VIEWING_MY_TITLES: - response = BotHandlers::returnMyTitles(ctx.cursor); - break; - case UserState::VIEWING_REVIEW_LIST: - response = BotHandlers::returnReviewList(ctx.cursor); - break; - default: - botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR); - return; - } - - botApi.editMessageText( - response.message, - query->message->chat->id, - query->message->messageId, - "", - "", - nullptr, - response.keyboard - ); -} - void BotHandlers::handleMessage(TgBot::Message::Ptr message) { //TODO: просмотр состояния пользователя return; @@ -90,6 +52,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { auto it = userContexts.find(userId); if (it == userContexts.end()) { // TODO: log + sendError(query, BotConstants::Text::AUTH_ERROR); std::cout << "Error: Не нашел пользователя " << userId; return; } @@ -127,7 +90,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { // Обработка back по интерфейсу if (data == BotConstants::Callback::NAV_BACK) { if (!popState(ctx)) { - botApi.answerCallbackQuery(query->id, "Некуда возвращаться", true); + sendError(query, BotConstants::Text::SAD_ERROR); return; } auto result = renderCurrent(ctx); @@ -138,7 +101,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { // Переходы вперёд (push) auto newStepOpt = computeNextStep(query, current); if (!newStepOpt.has_value()) { - sendError(query->message->chat->id); + sendError(query, BotConstants::Text::SAD_ERROR); return; } @@ -181,4 +144,93 @@ void BotHandlers::increasePayload(int64_t& payload, const UserState curState) { payload = BotConstants::NULL_PAYLOAD; std::cerr << "Error: increasePayload" << std::endl; } +} + +void BotHandlers::editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult response) { + botApi.editMessageText( + response.message, + query->message->chat->id, + query->message->messageId, + "", + "", + nullptr, + response.keyboard + ); +} + +HandlerResult BotHandlers::renderCurrent(const UserContext& ctx) { + const auto& step = ctx.history.back(); + switch (step.state) { + case UserState::MAIN_MENU: + return showMainMenu(); + case UserState::VIEWING_MY_TITLES: + return returnMyTitles(ctx.userId, step.payload); // payload = offset + /* + case UserState::VIEWING_TITLE_PAGE: + return returnTitlePage(step.payload); // payload = titleId + case UserState::VIEWING_REVIEW: + return returnReview(step.payload); // payload = reviewId + case UserState::AWAITING_REVIEW: + return HandlerResult{"Пришлите текст отзыва:", nullptr}; + // ... + */ + default: + return HandlerResult{BotConstants::Text::SAD_ERROR, nullptr}; + } +} + +HandlerResult BotHandlers::showMainMenu() { + auto keyboard = KeyboardFactory::createMainMenu(); + + return HandlerResult{BotConstants::Text::MAIN_MENU, keyboard}; +} + +void BotHandlers::sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText) { + //TODO: посылать сообщение с кнопкой возврата в главное меню + auto keyboard = nullptr; + + editMessage(query, {errText, keyboard}); +} + +std::optional<NavigationStep> BotHandlers::computeNextStep( + const TgBot::CallbackQuery::Ptr& query, + const NavigationStep& current +) { + const std::string& data = query->data; + + switch (current.state) { + case UserState::MAIN_MENU: + if (data == BotConstants::Callback::MY_TITLES) { + return NavigationStep{UserState::VIEWING_MY_TITLES, 0}; + } + break; + /* + case UserState::VIEWING_MY_TITLES: + if (data.starts_with("title_")) { + int64_t titleId = parseId(data); + return NavigationStep{UserState::VIEWING_TITLE_PAGE, titleId}; + } + break; + + case UserState::VIEWING_TITLE_PAGE: + if (data == BotConstants::Callback::ACTION_ADD_REVIEW) { + return NavigationStep{UserState::AWAITING_REVIEW, current.payload}; + } + if (data.starts_with("review_")) { + int64_t reviewId = parseId(data); + return NavigationStep{UserState::VIEWING_REVIEW, reviewId}; + } + break; + + // Добавляйте другие переходы по мере роста функционала + */ + default: + break; + } + return std::nullopt; +} + +void BotHandlers::createInitContext(int64_t chatId) { + NavigationStep init = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD}; + userContexts[chatId] = {chatId, {init}}; } \ No newline at end of file From 6123ee039b8f21d4dfa7aab14edb1a92b572bbeb Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Thu, 4 Dec 2025 16:38:18 +0300 Subject: [PATCH 20/33] feat(tgbot-front): start handleError func develop --- modules/bot/front/include/KeyboardFactory.hpp | 3 ++ modules/bot/front/include/constants.hpp | 5 ++ modules/bot/front/include/handlers.hpp | 4 +- modules/bot/front/src/KeyboardFactory.cpp | 10 ++++ modules/bot/front/src/handlers.cpp | 49 ++++++++++++++----- 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/modules/bot/front/include/KeyboardFactory.hpp b/modules/bot/front/include/KeyboardFactory.hpp index 0d3d5f0..71e5d10 100644 --- a/modules/bot/front/include/KeyboardFactory.hpp +++ b/modules/bot/front/include/KeyboardFactory.hpp @@ -8,4 +8,7 @@ public: /// Create keyboard for My_Titles static TgBot::InlineKeyboardMarkup::Ptr createMyTitles(std::vector<Title> titles); + + /// Create keyboard for sendError + static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback); }; diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 8fee9d5..516bc54 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -8,6 +8,7 @@ namespace BotConstants { const int64_t DISP_REVIEW_NUM = 4; // Количество ревью, отображаемых на страничке namespace Button { + const std::string TO_MAIN_MENU = "Главное меню"; const std::string FIND_ANIME = "Найти аниме"; const std::string MY_TITLES = "Мои тайтлы"; const std::string PREV = "<<Назад"; @@ -24,11 +25,15 @@ namespace BotConstants { const std::string WANT = STATUS + "want"; const std::string THROWN = STATUS + "thrown"; const std::string NAVIGATION = "navigation:"; + const std::string MAIN_MENU = NAVIGATION + "main_menu"; const std::string MY_TITLES = NAVIGATION + "my_titles"; const std::string LIST_PREV = NAVIGATION + "prev"; // Пагинация const std::string LIST_NEXT = NAVIGATION + "next"; // Пагинация const std::string NAV_BACK = NAVIGATION + "back"; // Возврат по стеку состояний const std::string CHOICE = "choice:"; + const std::string ERROR = "error:"; + const std::string ERROR_NAVIGATION = ERROR + NAVIGATION; + const std::string ERROR_AUTH = ERROR + "auth"; } namespace Text { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 572984d..60ac79d 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -59,7 +59,9 @@ private: TgBot::Api botApi; std::unordered_map<int64_t, UserContext> userContexts; - void handleNavigation(TgBot::CallbackQuery::Ptr query); + void handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx); + + void handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx); void processCallbackImpl(TgBot::CallbackQuery::Ptr query); diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index cf2500c..e3b02b4 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -67,3 +67,13 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Tit return keyboard; } + +TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createError(const std::string& errorCallback) { + auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>(); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = BotConstants::Button::TO_MAIN_MENU; + button->callbackData = errorCallback; + + keyboard->inlineKeyboard = {{button}}; + return keyboard; +} diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 5bdef9d..04b2e8b 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -38,16 +38,6 @@ void BotHandlers::handleMessage(TgBot::Message::Ptr message) { void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { const std::string& data = query->data; - - if (data.starts_with(BotConstants::Callback::NAVIGATION)) { - handleNavigation(query); - } - else { - botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); - } -} - -void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { int64_t userId = query->from->id; auto it = userContexts.find(userId); if (it == userContexts.end()) { @@ -58,6 +48,19 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { } UserContext& ctx = it->second; + + if (data.starts_with(BotConstants::Callback::NAVIGATION)) { + handleNavigation(query, ctx); + } + else if (data.starts_with(BotConstants::Callback::ERROR)) { + handleError(query, ctx); + } + else { + botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); + } +} + +void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { const auto& current = ctx.history.back(); // текущий экран const std::string& data = query->data; @@ -187,7 +190,13 @@ HandlerResult BotHandlers::showMainMenu() { void BotHandlers::sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText) { //TODO: посылать сообщение с кнопкой возврата в главное меню - auto keyboard = nullptr; + TgBot::InlineKeyboardMarkup::Ptr keyboard; + if (errText == BotConstants::Text::SAD_ERROR) { + keyboard = KeyboardFactory::createError(BotConstants::Callback::ERROR_NAVIGATION); + } + else if (errText == BotConstants::Text::AUTH_ERROR) { + keyboard = nullptr; //KeyboardFactory::createError(BotConstants::Callback::ERROR_AUTH); + } editMessage(query, {errText, keyboard}); } @@ -221,8 +230,6 @@ std::optional<NavigationStep> BotHandlers::computeNextStep( return NavigationStep{UserState::VIEWING_REVIEW, reviewId}; } break; - - // Добавляйте другие переходы по мере роста функционала */ default: break; @@ -233,4 +240,20 @@ std::optional<NavigationStep> BotHandlers::computeNextStep( void BotHandlers::createInitContext(int64_t chatId) { NavigationStep init = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD}; userContexts[chatId] = {chatId, {init}}; +} + +void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { + const std::string& data = query->data; + + if(data == BotConstants::Callback::ERROR_NAVIGATION) { + ctx.history.clear(); + ctx.history.push_back({UserState::MAIN_MENU, 0}); + auto result = showMainMenu(); + editMessage(query, result); + } + else if(data == BotConstants::Callback::ERROR_AUTH) { + // TODO: продумать логику + HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr}; + editMessage(query, result); + } } \ No newline at end of file From 847aec7bdd3816262b31ceaf22d79042e0346de0 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 5 Dec 2025 12:38:34 +0300 Subject: [PATCH 21/33] feat(tgbot-back): start to develop back Implemented fetchUserTitlesAsync func and embedded it in the code of the front in the trial mode. It needs to be restructured --- modules/bot/back/include/BotToServer.hpp | 31 +++++++ modules/bot/back/src/BotToServer.cpp | 93 +++++++++++++++++++ modules/bot/front/CMakeLists.txt | 16 +++- modules/bot/front/include/KeyboardFactory.hpp | 2 +- modules/bot/front/include/constants.hpp | 1 + modules/bot/front/include/handlers.hpp | 7 +- modules/bot/front/include/structs.hpp | 2 + modules/bot/front/src/KeyboardFactory.cpp | 4 +- modules/bot/front/src/handlers.cpp | 47 ++++++++-- 9 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 modules/bot/back/include/BotToServer.hpp create mode 100644 modules/bot/back/src/BotToServer.cpp diff --git a/modules/bot/back/include/BotToServer.hpp b/modules/bot/back/include/BotToServer.hpp new file mode 100644 index 0000000..7f10fb6 --- /dev/null +++ b/modules/bot/back/include/BotToServer.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "CppRestOpenAPIClient/ApiClient.h" +#include "CppRestOpenAPIClient/ApiConfiguration.h" +#include "CppRestOpenAPIClient/api/DefaultApi.h" +#include "CppRestOpenAPIClient/model/User.h" +#include "CppRestOpenAPIClient/model/GetUserTitles_200_response.h" +#include "CppRestOpenAPIClient/model/UserTitle.h" +#include "CppRestOpenAPIClient/model/Title.h" +#include "constants.hpp" +#include "structs.hpp" +#include <iostream> +#include <thread> +#include <functional> +#include <memory> +#include <cpprest/asyncrt_utils.h> +#include <boost/optional.hpp> + +using namespace org::openapitools::client::api; + +class BotToServer { +public: + BotToServer(); + + // Асинхронный метод: получить список тайтлов пользователя + pplx::task<std::vector<BotStructs::Title>> fetchUserTitlesAsync(const std::string& userId); + +private: + std::shared_ptr<org::openapitools::client::api::ApiConfiguration> apiconfiguration; + std::shared_ptr<org::openapitools::client::api::ApiClient> apiclient; + std::shared_ptr<org::openapitools::client::api::DefaultApi> api; +}; \ No newline at end of file diff --git a/modules/bot/back/src/BotToServer.cpp b/modules/bot/back/src/BotToServer.cpp new file mode 100644 index 0000000..8897ac4 --- /dev/null +++ b/modules/bot/back/src/BotToServer.cpp @@ -0,0 +1,93 @@ +#include "BotToServer.hpp" + +BotToServer::BotToServer() { + apiconfiguration = std::make_shared<ApiConfiguration>(); + const char* envUrl = getenv("NYANIMEDBBASEURL"); + if (!envUrl) { + std::runtime_error("Environment variable NYANIMEDBBASEURL is not set"); + } + apiconfiguration->setBaseUrl(utility::conversions::to_string_t(envUrl)); + apiconfiguration->setUserAgent(utility::conversions::to_string_t("OpenAPI Client")); + apiclient = std::make_shared<ApiClient>(apiconfiguration); + api = std::make_shared<DefaultApi>(apiclient); +} + +// Вспомогательная функция: преобразует UserTitle → BotStructs::Title +static BotStructs::Title mapUserTitleToBotTitle( + const std::shared_ptr<org::openapitools::client::model::UserTitle>& userTitle +) { + if (!userTitle || !userTitle->titleIsSet()) { + return BotStructs::Title{0, "Invalid", "", -1}; + } + + auto apiTitle = userTitle->getTitle(); + if (!apiTitle) { + return BotStructs::Title{0, "No Title", "", -1}; + } + + int64_t id = apiTitle->getId(); + + std::string name = "Untitled"; + auto titleNames = apiTitle->getTitleNames(); + utility::string_t ru = U("ru"); + + if (titleNames.find(ru) != titleNames.end() && !titleNames.at(ru).empty()) { + name = utility::conversions::to_utf8string(titleNames.at(ru).front()); + } else if (!titleNames.empty()) { + const auto& firstLang = *titleNames.begin(); + if (!firstLang.second.empty()) { + name = utility::conversions::to_utf8string(firstLang.second.front()); + } + } + + std::string description = ""; // описание пока не поддерживается в OpenAPI-модели + return BotStructs::Title{id, name, description, -1}; +} + +pplx::task<std::vector<BotStructs::Title>> BotToServer::fetchUserTitlesAsync(const std::string& userId) { + utility::string_t userIdW = utility::conversions::to_string_t(userId); + int32_t limit = static_cast<int32_t>(BotConstants::DISP_TITLES_NUM); + + auto responseTask = api->getUserTitles( + userIdW, + boost::none, // cursor + boost::none, // sort + boost::none, // sortForward + boost::none, // word + boost::none, // status + boost::none, // watchStatus + boost::none, // rating + boost::none, // myRate + boost::none, // releaseYear + boost::none, // releaseSeason + limit, + boost::none // fields + ); + + return responseTask.then([=](pplx::task<std::shared_ptr<org::openapitools::client::model::GetUserTitles_200_response>> task) { + try { + auto response = task.get(); + if (!response) { + throw std::runtime_error("Null response from getUserTitles"); + } + + const auto& userTitles = response->getData(); + std::vector<BotStructs::Title> result; + result.reserve(userTitles.size()); + + for (size_t i = 0; i < userTitles.size(); ++i) { + BotStructs::Title botTitle = mapUserTitleToBotTitle(userTitles[i]); + botTitle.num = static_cast<int64_t>(i); // 0-based индекс + result.push_back(botTitle); + } + + return result; + } catch (const web::http::http_exception& e) { + std::cerr << "HTTP error in fetchUserTitlesAsync: " << e.what() << std::endl; + throw; + } catch (const std::exception& e) { + std::cerr << "Error in fetchUserTitlesAsync: " << e.what() << std::endl; + throw; + } + }); +} \ No newline at end of file diff --git a/modules/bot/front/CMakeLists.txt b/modules/bot/front/CMakeLists.txt index 19ee60a..f155517 100644 --- a/modules/bot/front/CMakeLists.txt +++ b/modules/bot/front/CMakeLists.txt @@ -1,7 +1,16 @@ cmake_minimum_required(VERSION 3.10.2) project(AnimeBot) -file(GLOB SOURCES "src/*.cpp") +set(SOURCES "") +file(GLOB_RECURSE SRC_FRONT "src/*.cpp") +list(APPEND SOURCES ${SRC_FRONT}) + +file(GLOB_RECURSE SRC_BACK "../back/src/*.cpp") +list(APPEND SOURCES ${SRC_BACK}) + +file(GLOB_RECURSE SRC_API "../generated-client/src/*.cpp") +list(APPEND SOURCES ${SRC_API}) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") @@ -12,8 +21,11 @@ find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) find_package(Boost COMPONENTS system REQUIRED) find_package(CURL) +find_library(CPPREST_LIB cpprest REQUIRED) include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR}) include_directories(include/) +include_directories(../back/include) +include_directories(../generated-client/include) if (CURL_FOUND) include_directories(${CURL_INCLUDE_DIRS}) add_definitions(-DHAVE_CURL) @@ -21,4 +33,4 @@ endif() add_executable(AnimeBot ${SOURCES}) -target_link_libraries(AnimeBot /usr/local/lib/libTgBot.a ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES} ${Boost_LIBRARIES} ${CURL_LIBRARIES}) +target_link_libraries(AnimeBot /usr/local/lib/libTgBot.a ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES} ${Boost_LIBRARIES} ${CURL_LIBRARIES} ${CPPREST_LIB}) diff --git a/modules/bot/front/include/KeyboardFactory.hpp b/modules/bot/front/include/KeyboardFactory.hpp index 71e5d10..f51b5a1 100644 --- a/modules/bot/front/include/KeyboardFactory.hpp +++ b/modules/bot/front/include/KeyboardFactory.hpp @@ -7,7 +7,7 @@ public: static TgBot::InlineKeyboardMarkup::Ptr createMainMenu(); /// Create keyboard for My_Titles - static TgBot::InlineKeyboardMarkup::Ptr createMyTitles(std::vector<Title> titles); + static TgBot::InlineKeyboardMarkup::Ptr createMyTitles(std::vector<BotStructs::Title> titles); /// Create keyboard for sendError static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback); diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 516bc54..50427f5 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -39,5 +39,6 @@ namespace BotConstants { const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся"; const std::string AUTH_ERROR = "Проблемы с авторизацией, попробуйте авторизоваться повторно"; + const std::string SERVER_ERROR = "Не удалось загрузить данные. Попробуйте позже."; } } diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 60ac79d..cb462cd 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -3,6 +3,7 @@ #include <string> #include <structs.hpp> #include <unordered_map> +#include "BotToServer.hpp" /// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения struct HandlerResult { @@ -58,6 +59,7 @@ public: private: TgBot::Api botApi; std::unordered_map<int64_t, UserContext> userContexts; + BotToServer server_; void handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx); @@ -104,7 +106,7 @@ private: /// @brief Отрисовка текущего экрана (соотв. контексту) /// @param ctx - текущий контекст /// @return HandlerResult для нового состояния сообщения - HandlerResult renderCurrent(const UserContext& ctx); + HandlerResult renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx); /// @brief Логика переходов между контекстами (навигация на следующий шаг) /// @param query - запрос @@ -120,4 +122,7 @@ private: /// @brief Посылает интерфейс обработки ошибки на callback запрос /// @param query запрос void sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText); + + // Форматирование для отображения в сообщении + std::string formatTitlesList(const std::vector<BotStructs::Title>& titles); }; diff --git a/modules/bot/front/include/structs.hpp b/modules/bot/front/include/structs.hpp index 57f5e4a..d702163 100644 --- a/modules/bot/front/include/structs.hpp +++ b/modules/bot/front/include/structs.hpp @@ -1,8 +1,10 @@ #pragma once +namespace BotStructs { struct Title { int64_t id; std::string name; std::string description; int64_t num; }; +} diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index e3b02b4..af6c1d8 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -15,13 +15,13 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() { } // TODO: Переписать с учетом констант на количество отображаемых тайтлов и нового callback'a -TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Title> titles) { +TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<BotStructs::Title> titles) { auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>(); std::vector<TgBot::InlineKeyboardButton::Ptr> row; std::vector<std::vector<TgBot::InlineKeyboardButton::Ptr>> layout; int counter = 0; - for(Title& title : titles) { + for(BotStructs::Title& title : titles) { if(counter >= 6) { break; } diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 04b2e8b..88e026e 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -22,7 +22,7 @@ void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) { HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) { // Здесь должен происходить запрос на сервер - std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; + std::vector<BotStructs::Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; struct HandlerResult result; result.keyboard = KeyboardFactory::createMyTitles(titles); @@ -85,7 +85,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx.history.back().payload = newPayload; - auto result = renderCurrent(ctx); + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать editMessage(query, result); return; } @@ -96,7 +97,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& sendError(query, BotConstants::Text::SAD_ERROR); return; } - auto result = renderCurrent(ctx); + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать editMessage(query, result); return; } @@ -109,7 +111,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& } ctx.history.push_back(*newStepOpt); - auto result = renderCurrent(ctx); + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать editMessage(query, result); } @@ -161,15 +164,32 @@ void BotHandlers::editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult res ); } -HandlerResult BotHandlers::renderCurrent(const UserContext& ctx) { +HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { const auto& step = ctx.history.back(); + int64_t userId = query->from->id; switch (step.state) { case UserState::MAIN_MENU: return showMainMenu(); case UserState::VIEWING_MY_TITLES: - return returnMyTitles(ctx.userId, step.payload); // payload = offset + server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId + .then([this, query](pplx::task<std::vector<BotStructs::Title>> t) { + try { + auto titles = t.get(); + + std::string message = formatTitlesList(titles); + auto keyboard = KeyboardFactory::createMyTitles(titles); + + editMessage(query, {message, keyboard}); + + } catch (const std::exception& e) { + sendError(query, BotConstants::Text::SERVER_ERROR); + // Логирование ошибки (например, в cerr) + } + }); + + return {"meow", nullptr}; /* - case UserState::VIEWING_TITLE_PAGE: + case UserState::VIEWING_TITLE_PAGE: return returnTitlePage(step.payload); // payload = titleId case UserState::VIEWING_REVIEW: return returnReview(step.payload); // payload = reviewId @@ -256,4 +276,17 @@ void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr}; editMessage(query, result); } +} + +std::string BotHandlers::formatTitlesList(const std::vector<BotStructs::Title>& titles) { + if (titles.empty()) { + return "У вас пока нет тайтлов."; + } + + std::string msg; + for (size_t i = 0; i < titles.size(); ++i) { + // num — 0-based, но в сообщении показываем 1-based + msg += std::to_string(i + 1) + ". " + titles[i].name + "\n"; + } + return msg; } \ No newline at end of file From ba4dfec459688ecab09845275fb4500cce51d2ba Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 5 Dec 2025 20:28:03 +0300 Subject: [PATCH 22/33] refactor(tgbot): change the location of the CMakeLists.txt --- modules/bot/.gitignore | 2 ++ modules/bot/{front => }/CMakeLists.txt | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 modules/bot/.gitignore rename modules/bot/{front => }/CMakeLists.txt (77%) diff --git a/modules/bot/.gitignore b/modules/bot/.gitignore new file mode 100644 index 0000000..bf53375 --- /dev/null +++ b/modules/bot/.gitignore @@ -0,0 +1,2 @@ +generated-client/ +out/ \ No newline at end of file diff --git a/modules/bot/front/CMakeLists.txt b/modules/bot/CMakeLists.txt similarity index 77% rename from modules/bot/front/CMakeLists.txt rename to modules/bot/CMakeLists.txt index f155517..7a8948d 100644 --- a/modules/bot/front/CMakeLists.txt +++ b/modules/bot/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.10.2) project(AnimeBot) set(SOURCES "") -file(GLOB_RECURSE SRC_FRONT "src/*.cpp") +file(GLOB_RECURSE SRC_FRONT "front/src/*.cpp") list(APPEND SOURCES ${SRC_FRONT}) -file(GLOB_RECURSE SRC_BACK "../back/src/*.cpp") +file(GLOB_RECURSE SRC_BACK "back/src/*.cpp") list(APPEND SOURCES ${SRC_BACK}) -file(GLOB_RECURSE SRC_API "../generated-client/src/*.cpp") +file(GLOB_RECURSE SRC_API "generated-client/src/*.cpp") list(APPEND SOURCES ${SRC_API}) set(CMAKE_CXX_STANDARD 20) @@ -23,9 +23,9 @@ find_package(Boost COMPONENTS system REQUIRED) find_package(CURL) find_library(CPPREST_LIB cpprest REQUIRED) include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR}) -include_directories(include/) -include_directories(../back/include) -include_directories(../generated-client/include) +include_directories(front/include/) +include_directories(back/include) +include_directories(generated-client/include) if (CURL_FOUND) include_directories(${CURL_INCLUDE_DIRS}) add_definitions(-DHAVE_CURL) From 20cf8b1fc21d7b3c45080f08a432303ed32c9262 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 5 Dec 2025 22:49:14 +0300 Subject: [PATCH 23/33] build(tgbot-gen): Changes and additions required to generate the API client with auth (cookie) I use a standard set of templates, changes are made in api-header.mustache and api-source.mustache. Their _old versions contain the originals. Additions are necessary for the header argument in the generated functions so that you can authenticate using cookies. --- .../cpp-restsdk/README.mustache | 59 ++ .../cpp-restsdk/anytype-header.mustache | 46 ++ .../cpp-restsdk/anytype-source.mustache | 40 ++ .../cpp-restsdk/api-gmock.mustache | 41 ++ .../cpp-restsdk/api-header.mustache | 86 +++ .../cpp-restsdk/api-header.mustache_old | 84 +++ .../cpp-restsdk/api-source.mustache | 375 ++++++++++ .../cpp-restsdk/api-source.mustache_old | 368 ++++++++++ .../cpp-restsdk/apiclient-header.mustache | 107 +++ .../cpp-restsdk/apiclient-source.mustache | 203 ++++++ .../apiconfiguration-header.mustache | 54 ++ .../apiconfiguration-source.mustache | 73 ++ .../cpp-restsdk/apiexception-header.mustache | 48 ++ .../cpp-restsdk/apiexception-source.mustache | 41 ++ .../cpp-restsdk/cmake-config.mustache | 5 + .../cpp-restsdk/cmake-lists.mustache | 91 +++ .../cpp-restsdk/git_push.sh.mustache | 57 ++ .../cpp-restsdk/gitignore.mustache | 29 + .../cpp-restsdk/httpcontent-header.mustache | 57 ++ .../cpp-restsdk/httpcontent-source.mustache | 74 ++ .../cpp-restsdk/ihttpbody-header.mustache | 30 + .../cpp-restsdk/jsonbody-header.mustache | 37 + .../cpp-restsdk/jsonbody-source.mustache | 24 + .../cpp-restsdk/licenseInfo.mustache | 15 + .../cpp-restsdk/model-header.mustache | 288 ++++++++ .../cpp-restsdk/model-source.mustache | 457 ++++++++++++ .../cpp-restsdk/modelbase-header.mustache | 493 +++++++++++++ .../cpp-restsdk/modelbase-source.mustache | 653 ++++++++++++++++++ .../cpp-restsdk/multipart-header.mustache | 49 ++ .../cpp-restsdk/multipart-source.mustache | 100 +++ .../cpp-restsdk/object-header.mustache | 50 ++ .../cpp-restsdk/object-source.mustache | 79 +++ 32 files changed, 4213 insertions(+) create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/README.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/anytype-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/anytype-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/api-gmock.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache_old create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache_old create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiclient-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiclient-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiexception-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/apiexception-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/cmake-config.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/cmake-lists.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/git_push.sh.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/gitignore.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/httpcontent-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/httpcontent-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/ihttpbody-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/jsonbody-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/jsonbody-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/licenseInfo.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/model-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/model-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/modelbase-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/modelbase-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/multipart-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/multipart-source.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/object-header.mustache create mode 100644 api/_build/my-cpp-templates/cpp-restsdk/object-source.mustache diff --git a/api/_build/my-cpp-templates/cpp-restsdk/README.mustache b/api/_build/my-cpp-templates/cpp-restsdk/README.mustache new file mode 100644 index 0000000..6a59d00 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/README.mustache @@ -0,0 +1,59 @@ +# C++ API client + +{{#appDescriptionWithNewLines}} +{{{.}}} +{{/appDescriptionWithNewLines}} + +## Overview +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI spec](https://openapis.org) from a remote server, you can easily generate an API client. + +- API version: {{appVersion}} +- Package version: {{packageVersion}} +{{^hideGenerationTimestamp}} +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} +- Generator version: {{generatorVersion}} +- Build package: {{generatorClass}} +{{#infoUrl}} +For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +- API namespace: {{{apiPackage}}} +- Model namespace: {{{modelPackage}}} + +## Installation + +### Prerequisites + +Install [cpprestsdk](https://github.com/Microsoft/cpprestsdk). + +- Windows: `vcpkg install cpprestsdk cpprestsdk:x64-windows boost-uuid boost-uuid:x64-windows` +- Mac: `brew install cpprestsdk` +- Linux: `sudo apt-get install libcpprest-dev` + +### Build + +```sh +cmake -DCPPREST_ROOT=/usr -DCMAKE_CXX_FLAGS="-I/usr/local/opt/openssl/include" -DCMAKE_MODULE_LINKER_FLAGS="-L/usr/local/opt/openssl/lib" +make +``` + +### Build on Windows with Visual Studio (VS2017) + +- Right click on folder containing source code +- Select 'Open in visual studio' +- Once visual studio opens, CMake should show up in top menu bar. +- Select CMake > Build All. + +*Note: If the CMake menu item doesn't show up in Visual Studio, CMake +for Visual Studio must be installed. In this case, open the 'Visual Studio +Installer' application. Select 'modify' Visual Studio 2017. Make sure +'Desktop Development with C++' is installed, and specifically that 'Visual +C++ tools for CMake' is selected in the 'Installation Details' section. + +Also be sure to review the CMakeLists.txt file. Edits are likely required.* + +## Author + +{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}} +{{/-last}}{{/apis}}{{/apiInfo}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/anytype-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/anytype-header.mustache new file mode 100644 index 0000000..16206ff --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/anytype-header.mustache @@ -0,0 +1,46 @@ +{{>licenseInfo}} +/* + * AnyType.h + * + * This is the implementation of an any JSON type. + */ + +#ifndef {{modelHeaderGuardPrefix}}_AnyType_H_ +#define {{modelHeaderGuardPrefix}}_AnyType_H_ + +{{{defaultInclude}}} +#include "{{packageName}}/Object.h" + +#include <cpprest/details/basic_types.h> +#include <cpprest/json.h> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} AnyType : public Object { +public: + AnyType(); + virtual ~AnyType(); + + ///////////////////////////////////////////// + /// ModelBase overrides + void validate() override; + + web::json::value toJson() const override; + bool fromJson(const web::json::value &json) override; + + void toMultipart(std::shared_ptr<MultipartFormData> multipart, + const utility::string_t &namePrefix) const override; + bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, + const utility::string_t &namePrefix) override; + +private: + web::json::value m_value; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_AnyType_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/anytype-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/anytype-source.mustache new file mode 100644 index 0000000..8f9421a --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/anytype-source.mustache @@ -0,0 +1,40 @@ +{{>licenseInfo}} +#include "{{packageName}}/AnyType.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +AnyType::AnyType() { m_value = web::json::value::null(); } + +AnyType::~AnyType() {} + +void AnyType::validate() {} + +web::json::value AnyType::toJson() const { return m_value; } + +bool AnyType::fromJson(const web::json::value &val) { + m_value = val; + m_IsSet = true; + return isSet(); +} + +void AnyType::toMultipart(std::shared_ptr<MultipartFormData> multipart, + const utility::string_t &prefix) const { + if (m_value.is_object()) { + return Object::toMultipart(multipart, prefix); + } + throw std::runtime_error("AnyType::toMultipart: unsupported type"); +} + +bool AnyType::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, + const utility::string_t &prefix) { + if (m_value.is_object()) { + return Object::fromMultiPart(multipart, prefix); + } + return false; +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/api-gmock.mustache b/api/_build/my-cpp-templates/cpp-restsdk/api-gmock.mustache new file mode 100644 index 0000000..d144bf6 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/api-gmock.mustache @@ -0,0 +1,41 @@ +{{>licenseInfo}} +{{#operations}} +#ifndef {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_ +#define {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_ + +#include <gmock/gmock.h> + +#include "{{classname}}.h" + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + + +class {{declspec}} {{classname}}Mock : public I{{classname}} +{ +public: + using Base = I{{classname}}; + + {{classname}}Mock() = default; + explicit {{classname}}Mock( std::shared_ptr<ApiClient> apiClient ) { }; + ~{{classname}}Mock() override = default; + + {{#operation}} + MOCK_METHOD{{allParams.size}}( {{operationId}}, pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> ( + {{#allParams}} + {{^required}}boost::optional<{{/required}}{{#isFile}}std::shared_ptr<{{/isFile}}{{{dataType}}}{{#isFile}}>{{/isFile}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}} + {{/allParams}} + ) ); + {{/operation}} +}; + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif /* {{apiHeaderGuardPrefix}}_{{classname}}GMock_H_ */ + +{{/operations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache new file mode 100644 index 0000000..a4e60e1 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache @@ -0,0 +1,86 @@ +{{>licenseInfo}} +{{#operations}}/* + * {{classname}}.h + * + * {{description}} + */ + +#ifndef {{apiHeaderGuardPrefix}}_{{classname}}_H_ +#define {{apiHeaderGuardPrefix}}_{{classname}}_H_ + +{{{defaultInclude}}} + +#include "{{packageName}}/ApiClient.h" +{{^hasModelImport}}#include "{{packageName}}/ModelBase.h"{{/hasModelImport}} +{{#imports}}{{{import}}} +{{/imports}} +#include <boost/optional.hpp> +#include <map> // <-- добавлено для std::map + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +{{#gmockApis}} +class {{declspec}} I{{classname}} +{ +public: + I{{classname}}() = default; + virtual ~I{{classname}}() = default; + + {{#operation}} + virtual pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}( + {{#allParams}} + {{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}},{{/allParams}} + const std::map<utility::string_t, utility::string_t>& customHeaders = {} // <-- добавлено + ) const = 0; + {{/operation}} +};{{/gmockApis}} + +class {{declspec}} {{classname}} {{#gmockApis}} : public I{{classname}} {{/gmockApis}} +{ +public: + {{#gmockApis}} + using Base = I{{classname}}; + {{/gmockApis}} + + explicit {{classname}}( std::shared_ptr<const ApiClient> apiClient ); + + {{#gmockApis}} + ~{{classname}}() override; + {{/gmockApis}} + {{^gmockApis}} + virtual ~{{classname}}(); + {{/gmockApis}} + + {{#operation}} + /// <summary> + /// {{summary}} + /// </summary> + /// <remarks> + /// {{notes}} + /// </remarks> + {{#allParams}} + /// <param name="{{paramName}}">{{#lambda.multiline_comment_4}}{{description}}{{/lambda.multiline_comment_4}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param> + {{/allParams}} + /// <param name="customHeaders">Additional HTTP headers to send with the request (optional)</param> + pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}( + {{#allParams}} + {{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}},{{/allParams}} + const std::map<utility::string_t, utility::string_t>& customHeaders = {} // <-- добавлено + ) const{{#gmockApis}} override{{/gmockApis}}; + {{/operation}} + +protected: + std::shared_ptr<const ApiClient> m_ApiClient; +}; + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif /* {{apiHeaderGuardPrefix}}_{{classname}}_H_ */ + +{{/operations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache_old b/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache_old new file mode 100644 index 0000000..c2c6458 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/api-header.mustache_old @@ -0,0 +1,84 @@ +{{>licenseInfo}} +{{#operations}}/* + * {{classname}}.h + * + * {{description}} + */ + +#ifndef {{apiHeaderGuardPrefix}}_{{classname}}_H_ +#define {{apiHeaderGuardPrefix}}_{{classname}}_H_ + +{{{defaultInclude}}} + +#include "{{packageName}}/ApiClient.h" +{{^hasModelImport}}#include "{{packageName}}/ModelBase.h"{{/hasModelImport}} +{{#imports}}{{{import}}} +{{/imports}} +#include <boost/optional.hpp> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +{{#gmockApis}} +class {{declspec}} I{{classname}} +{ +public: + I{{classname}}() = default; + virtual ~I{{classname}}() = default; + + {{#operation}} + virtual pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}( + {{#allParams}} + {{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}} + {{/allParams}} + ) const = 0; + {{/operation}} +};{{/gmockApis}} + +class {{declspec}} {{classname}} {{#gmockApis}} : public I{{classname}} {{/gmockApis}} +{ +public: + {{#gmockApis}} + using Base = I{{classname}}; + {{/gmockApis}} + + explicit {{classname}}( std::shared_ptr<const ApiClient> apiClient ); + + {{#gmockApis}} + ~{{classname}}() override; + {{/gmockApis}} + {{^gmockApis}} + virtual ~{{classname}}(); + {{/gmockApis}} + + {{#operation}} + /// <summary> + /// {{summary}} + /// </summary> + /// <remarks> + /// {{notes}} + /// </remarks> + {{#allParams}} + /// <param name="{{paramName}}">{{#lambda.multiline_comment_4}}{{description}}{{/lambda.multiline_comment_4}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param> + {{/allParams}} + pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{operationId}}( + {{#allParams}} + {{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}},{{/-last}} + {{/allParams}} + ) const{{#gmockApis}} override{{/gmockApis}}; + {{/operation}} + +protected: + std::shared_ptr<const ApiClient> m_ApiClient; +}; + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif /* {{apiHeaderGuardPrefix}}_{{classname}}_H_ */ + +{{/operations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache new file mode 100644 index 0000000..aa6385d --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache @@ -0,0 +1,375 @@ +{{>licenseInfo}} +{{#operations}} + +#include "{{packageName}}/api/{{classname}}.h" +#include "{{packageName}}/IHttpBody.h" +#include "{{packageName}}/JsonBody.h" +#include "{{packageName}}/MultipartFormData.h" + +#include <boost/algorithm/string/replace.hpp> + +#include <unordered_set> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +{{classname}}::{{classname}}( std::shared_ptr<const ApiClient> apiClient ) + : m_ApiClient(apiClient) +{ +} + +{{classname}}::~{{classname}}() +{ +} + +{{#operation}} +pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{classname}}::{{operationId}}({{#allParams}}{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}, {{/allParams}}const std::map<utility::string_t, utility::string_t>& customHeaders) const // <-- изменено: все параметры с запятой, + customHeaders +{ +{{#allParams}}{{#required}}{{^isPrimitiveType}}{{^isContainer}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == nullptr) + { + throw ApiException(400, utility::conversions::to_string_t("Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}")); + } +{{/isContainer}}{{/isPrimitiveType}}{{/required}}{{/allParams}} + + std::shared_ptr<const ApiConfiguration> localVarApiConfiguration( m_ApiClient->getConfiguration() ); + utility::string_t localVarPath = utility::conversions::to_string_t("{{{path}}}"); + {{#pathParams}} + boost::replace_all(localVarPath, utility::conversions::to_string_t("{") + utility::conversions::to_string_t("{{baseName}}") + utility::conversions::to_string_t("}"), web::uri::encode_uri(ApiClient::parameterToString({{{paramName}}}))); + {{/pathParams}} + + std::map<utility::string_t, utility::string_t> localVarQueryParams; + std::map<utility::string_t, utility::string_t> localVarHeaderParams( localVarApiConfiguration->getDefaultHeaders() ); // <-- уже содержит defaultHeaders + std::map<utility::string_t, utility::string_t> localVarFormParams; + std::map<utility::string_t, std::shared_ptr<HttpContent>> localVarFileParams; + + std::unordered_set<utility::string_t> localVarResponseHttpContentTypes; + {{#produces}} + localVarResponseHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") ); + {{/produces}} + + utility::string_t localVarResponseHttpContentType; + + // use JSON if possible + if ( localVarResponseHttpContentTypes.size() == 0 ) + { + {{#vendorExtensions.x-codegen-response.isString}} + localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain"); + {{/vendorExtensions.x-codegen-response.isString}} + {{^vendorExtensions.x-codegen-response.isString}} + localVarResponseHttpContentType = utility::conversions::to_string_t("application/json"); + {{/vendorExtensions.x-codegen-response.isString}} + } + // JSON + else if ( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("application/json"); + } + // multipart formdata + else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("multipart/form-data"); + } + {{#vendorExtensions.x-codegen-response.isString}} + // plain text + else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("text/plain")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain"); + } + {{/vendorExtensions.x-codegen-response.isString}} + {{#vendorExtensions.x-codegen-response-ishttpcontent}} + else + { + //It's going to be binary, so just use the first one. + localVarResponseHttpContentType = *localVarResponseHttpContentTypes.begin(); + } + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + {{^vendorExtensions.x-codegen-response-ishttpcontent}} + else + { + throw ApiException(400, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not produce any supported media type")); + } + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + + localVarHeaderParams[utility::conversions::to_string_t("Accept")] = localVarResponseHttpContentType; + + std::unordered_set<utility::string_t> localVarConsumeHttpContentTypes; + {{#consumes}} + localVarConsumeHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") ); + {{/consumes}} + + {{#allParams}} + {{^isBodyParam}} + {{^isPathParam}} + {{#required}} + {{^isPrimitiveType}} + {{^isContainer}} + if ({{paramName}} != nullptr) + {{/isContainer}} + {{/isPrimitiveType}} + {{/required}} + {{^required}} + {{^isPrimitiveType}} + {{^isContainer}} + if ({{paramName}} && *{{paramName}} != nullptr) + {{/isContainer}} + {{/isPrimitiveType}} + {{#isPrimitiveType}} + {{#isFile}} + if ({{paramName}} && *{{paramName}} != nullptr) + {{/isFile}} + {{^isFile}} + if ({{paramName}}) + {{/isFile}} + {{/isPrimitiveType}} + {{#isContainer}} + if ({{paramName}}) + {{/isContainer}} + {{/required}} + { + {{#isQueryParam}} + localVarQueryParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isQueryParam}} + {{#isHeaderParam}} + localVarHeaderParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isHeaderParam}} + {{#isFormParam}} + {{#isFile}} + localVarFileParams[ utility::conversions::to_string_t("{{baseName}}") ] = {{^required}}*{{/required}}{{paramName}}; + {{/isFile}} + {{^isFile}} + localVarFormParams[ utility::conversions::to_string_t("{{baseName}}") ] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isFile}} + {{/isFormParam}} + } + {{/isPathParam}} + {{/isBodyParam}} + {{/allParams}} + + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + // Apply customHeaders AFTER all other headers (so they can override) + for (const auto& header : customHeaders) { + localVarHeaderParams[header.first] = header.second; + } + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + std::shared_ptr<IHttpBody> localVarHttpBody; + utility::string_t localVarRequestHttpContentType; + + // use JSON if possible + if ( localVarConsumeHttpContentTypes.size() == 0 || localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarConsumeHttpContentTypes.end() ) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("application/json"); + {{#bodyParam}} + web::json::value localVarJson; + + {{#isPrimitiveType}} + localVarJson = ModelBase::toJson({{paramName}}{{^required}}.get(){{/required}}); + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isArray}} + { + std::vector<web::json::value> localVarJsonArray; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + {{#items.isPrimitiveType}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isString}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isString}}{{^items.isString}}{{#items.isDateTime}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isDateTime}}{{^items.isDateTime}}localVarJsonArray.push_back( localVarItem.get() ? localVarItem->toJson() : web::json::value::null() ); + {{/items.isDateTime}}{{/items.isString}}{{/items.isPrimitiveType}} + } + localVarJson = web::json::value::array(localVarJsonArray); + } + {{/isArray}} + {{^isArray}}{{#required}}localVarJson = ModelBase::toJson({{paramName}}); + {{/required}}{{^required}}if ({{paramName}}) + localVarJson = ModelBase::toJson(*{{paramName}});{{/required}} + {{/isArray}} + {{/isPrimitiveType}} + + localVarHttpBody = std::shared_ptr<IHttpBody>( new JsonBody( localVarJson ) ); + {{/bodyParam}} + } + // multipart formdata + else if( localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarConsumeHttpContentTypes.end() ) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("multipart/form-data"); + {{#bodyParam}} + std::shared_ptr<MultipartFormData> localVarMultipart(new MultipartFormData); + {{#isPrimitiveType}} + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}{{^required}}.get(){{/required}})); + {{/isPrimitiveType}}{{^isPrimitiveType}}{{#isArray}} + { + std::vector<web::json::value> localVarJsonArray; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + } + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonArray, utility::conversions::to_string_t("application/json"))); + }{{/isArray}}{{#isMap}} + { + std::map<utility::string_t, web::json::value> localVarJsonMap; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + web::json::value jval; + localVarJsonMap.insert( std::pair<utility::string_t, web::json::value>(localVarItem.first, ModelBase::toJson(localVarItem.second) )); + } + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonMap, utility::conversions::to_string_t("application/json"))); + }{{/isMap}} + {{^isArray}}{{^isMap}}{{#isString}}localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}})); + {{/isString}}{{^isString}}if({{^required}}{{paramName}} && (*{{paramName}}){{/required}}{{#required}}{{paramName}}{{/required}}.get()) + { + {{^required}}(*{{/required}}{{paramName}}{{^required}}){{/required}}->toMultipart(localVarMultipart, utility::conversions::to_string_t("{{paramName}}")); + } + {{/isString}} + {{/isMap}}{{/isArray}}{{/isPrimitiveType}} + + localVarHttpBody = localVarMultipart; + localVarRequestHttpContentType += utility::conversions::to_string_t("; boundary=") + localVarMultipart->getBoundary(); + {{/bodyParam}} + } + else if (localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/x-www-form-urlencoded")) != localVarConsumeHttpContentTypes.end()) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("application/x-www-form-urlencoded"); + } + else + { + throw ApiException(415, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not consume any supported media type")); + } + + {{#authMethods}} + // authentication ({{name}}) required + {{#isApiKey}} + {{#isKeyInHeader}} + { + utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}")); + if ( localVarApiKey.size() > 0 ) + { + localVarHeaderParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey; + } + } + {{/isKeyInHeader}} + {{#isKeyInQuery}} + { + utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}")); + if ( localVarApiKey.size() > 0 ) + { + localVarQueryParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey; + } + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{#isBasicBasic}} + // Basic authentication is added automatically as part of the http_client_config + {{/isBasicBasic}} + {{#isOAuth}} + // oauth2 authentication is added automatically as part of the http_client_config + {{/isOAuth}} + {{/authMethods}} + + return m_ApiClient->callApi(localVarPath, utility::conversions::to_string_t("{{httpMethod}}"), localVarQueryParams, localVarHttpBody, localVarHeaderParams, localVarFormParams, localVarFileParams, localVarRequestHttpContentType) + .then([=, this](web::http::http_response localVarResponse) + { + if (m_ApiClient->getResponseHandler()) + { + m_ApiClient->getResponseHandler()(localVarResponse.status_code(), localVarResponse.headers()); + } + + // 1xx - informational : OK + // 2xx - successful : OK + // 3xx - redirection : OK + // 4xx - client error : not OK + // 5xx - client error : not OK + if (localVarResponse.status_code() >= 400) + { + throw ApiException(localVarResponse.status_code() + , utility::conversions::to_string_t("error calling {{operationId}}: ") + localVarResponse.reason_phrase() + , std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get())); + } + + // check response content type + if(localVarResponse.headers().has(utility::conversions::to_string_t("Content-Type"))) + { + utility::string_t localVarContentType = localVarResponse.headers()[utility::conversions::to_string_t("Content-Type")]; + if( localVarContentType.find(localVarResponseHttpContentType) == std::string::npos ) + { + throw ApiException(500 + , utility::conversions::to_string_t("error calling {{operationId}}: unexpected response type: ") + localVarContentType + , std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get())); + } + } + + {{#vendorExtensions.x-codegen-response-ishttpcontent}} + return localVarResponse.extract_vector(); + }) + .then([=, this](std::vector<unsigned char> localVarResponse) + { + {{{returnType}}} localVarResult = std::make_shared<HttpContent>(); + std::shared_ptr<std::stringstream> stream = std::make_shared<std::stringstream>(std::string(localVarResponse.begin(), localVarResponse.end())); + localVarResult->setData(stream); + return localVarResult; + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + {{^vendorExtensions.x-codegen-response-ishttpcontent}} + return localVarResponse.extract_string(); + }) + .then([=, this](utility::string_t localVarResponse) + { + {{^returnType}} + return void(); + {{/returnType}} + {{#returnType}} + {{#returnContainer}} + {{{returnType}}} localVarResult; + {{/returnContainer}} + {{^returnContainer}} + {{{returnType}}} localVarResult({{{defaultResponse}}}); + {{/returnContainer}} + + if(localVarResponseHttpContentType == utility::conversions::to_string_t("application/json")) + { + web::json::value localVarJson = web::json::value::parse(localVarResponse); + {{#isArray}} + for( auto& localVarItem : localVarJson.as_array() ) + { + {{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj; + ModelBase::fromJson(localVarItem, localVarItemObj); + localVarResult.push_back(localVarItemObj); + }{{/isArray}}{{#isMap}} + for( auto& localVarItem : localVarJson.as_object() ) + { + {{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj; + ModelBase::fromJson(localVarItem.second, localVarItemObj); + localVarResult[localVarItem.first] = localVarItemObj; + }{{/isMap}}{{^isArray}}{{^isMap}} + ModelBase::fromJson(localVarJson, localVarResult);{{/isMap}}{{/isArray}} + }{{#vendorExtensions.x-codegen-response.isString}} + else if(localVarResponseHttpContentType == utility::conversions::to_string_t("text/plain")) + { + localVarResult = localVarResponse; + }{{/vendorExtensions.x-codegen-response.isString}} + // else if(localVarResponseHttpContentType == utility::conversions::to_string_t("multipart/form-data")) + // { + // TODO multipart response parsing + // } + else + { + throw ApiException(500 + , utility::conversions::to_string_t("error calling {{operationId}}: unsupported response type")); + } + + return localVarResult; + {{/returnType}} + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + }); +} +{{/operation}} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +{{/operations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache_old b/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache_old new file mode 100644 index 0000000..f4275fd --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/api-source.mustache_old @@ -0,0 +1,368 @@ +{{>licenseInfo}} +{{#operations}} + +#include "{{packageName}}/api/{{classname}}.h" +#include "{{packageName}}/IHttpBody.h" +#include "{{packageName}}/JsonBody.h" +#include "{{packageName}}/MultipartFormData.h" + +#include <boost/algorithm/string/replace.hpp> + +#include <unordered_set> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +{{classname}}::{{classname}}( std::shared_ptr<const ApiClient> apiClient ) + : m_ApiClient(apiClient) +{ +} + +{{classname}}::~{{classname}}() +{ +} + +{{#operation}} +pplx::task<{{{returnType}}}{{^returnType}}void{{/returnType}}> {{classname}}::{{operationId}}({{#allParams}}{{^required}}boost::optional<{{/required}}{{{dataType}}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) const +{ +{{#allParams}}{{#required}}{{^isPrimitiveType}}{{^isContainer}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == nullptr) + { + throw ApiException(400, utility::conversions::to_string_t("Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}")); + } +{{/isContainer}}{{/isPrimitiveType}}{{/required}}{{/allParams}} + + std::shared_ptr<const ApiConfiguration> localVarApiConfiguration( m_ApiClient->getConfiguration() ); + utility::string_t localVarPath = utility::conversions::to_string_t("{{{path}}}"); + {{#pathParams}} + boost::replace_all(localVarPath, utility::conversions::to_string_t("{") + utility::conversions::to_string_t("{{baseName}}") + utility::conversions::to_string_t("}"), web::uri::encode_uri(ApiClient::parameterToString({{{paramName}}}))); + {{/pathParams}} + + std::map<utility::string_t, utility::string_t> localVarQueryParams; + std::map<utility::string_t, utility::string_t> localVarHeaderParams( localVarApiConfiguration->getDefaultHeaders() ); + std::map<utility::string_t, utility::string_t> localVarFormParams; + std::map<utility::string_t, std::shared_ptr<HttpContent>> localVarFileParams; + + std::unordered_set<utility::string_t> localVarResponseHttpContentTypes; + {{#produces}} + localVarResponseHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") ); + {{/produces}} + + utility::string_t localVarResponseHttpContentType; + + // use JSON if possible + if ( localVarResponseHttpContentTypes.size() == 0 ) + { + {{#vendorExtensions.x-codegen-response.isString}} + localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain"); + {{/vendorExtensions.x-codegen-response.isString}} + {{^vendorExtensions.x-codegen-response.isString}} + localVarResponseHttpContentType = utility::conversions::to_string_t("application/json"); + {{/vendorExtensions.x-codegen-response.isString}} + } + // JSON + else if ( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("application/json"); + } + // multipart formdata + else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("multipart/form-data"); + } + {{#vendorExtensions.x-codegen-response.isString}} + // plain text + else if( localVarResponseHttpContentTypes.find(utility::conversions::to_string_t("text/plain")) != localVarResponseHttpContentTypes.end() ) + { + localVarResponseHttpContentType = utility::conversions::to_string_t("text/plain"); + } + {{/vendorExtensions.x-codegen-response.isString}} + {{#vendorExtensions.x-codegen-response-ishttpcontent}} + else + { + //It's going to be binary, so just use the first one. + localVarResponseHttpContentType = *localVarResponseHttpContentTypes.begin(); + } + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + {{^vendorExtensions.x-codegen-response-ishttpcontent}} + else + { + throw ApiException(400, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not produce any supported media type")); + } + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + + localVarHeaderParams[utility::conversions::to_string_t("Accept")] = localVarResponseHttpContentType; + + std::unordered_set<utility::string_t> localVarConsumeHttpContentTypes; + {{#consumes}} + localVarConsumeHttpContentTypes.insert( utility::conversions::to_string_t("{{{mediaType}}}") ); + {{/consumes}} + + {{#allParams}} + {{^isBodyParam}} + {{^isPathParam}} + {{#required}} + {{^isPrimitiveType}} + {{^isContainer}} + if ({{paramName}} != nullptr) + {{/isContainer}} + {{/isPrimitiveType}} + {{/required}} + {{^required}} + {{^isPrimitiveType}} + {{^isContainer}} + if ({{paramName}} && *{{paramName}} != nullptr) + {{/isContainer}} + {{/isPrimitiveType}} + {{#isPrimitiveType}} + {{#isFile}} + if ({{paramName}} && *{{paramName}} != nullptr) + {{/isFile}} + {{^isFile}} + if ({{paramName}}) + {{/isFile}} + {{/isPrimitiveType}} + {{#isContainer}} + if ({{paramName}}) + {{/isContainer}} + {{/required}} + { + {{#isQueryParam}} + localVarQueryParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isQueryParam}} + {{#isHeaderParam}} + localVarHeaderParams[utility::conversions::to_string_t("{{baseName}}")] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isHeaderParam}} + {{#isFormParam}} + {{#isFile}} + localVarFileParams[ utility::conversions::to_string_t("{{baseName}}") ] = {{^required}}*{{/required}}{{paramName}}; + {{/isFile}} + {{^isFile}} + localVarFormParams[ utility::conversions::to_string_t("{{baseName}}") ] = ApiClient::parameterToString({{^required}}*{{/required}}{{paramName}}); + {{/isFile}} + {{/isFormParam}} + } + {{/isPathParam}} + {{/isBodyParam}} + {{/allParams}} + + std::shared_ptr<IHttpBody> localVarHttpBody; + utility::string_t localVarRequestHttpContentType; + + // use JSON if possible + if ( localVarConsumeHttpContentTypes.size() == 0 || localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/json")) != localVarConsumeHttpContentTypes.end() ) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("application/json"); + {{#bodyParam}} + web::json::value localVarJson; + + {{#isPrimitiveType}} + localVarJson = ModelBase::toJson({{paramName}}{{^required}}.get(){{/required}}); + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isArray}} + { + std::vector<web::json::value> localVarJsonArray; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + {{#items.isPrimitiveType}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isString}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isString}}{{^items.isString}}{{#items.isDateTime}}localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + {{/items.isDateTime}}{{^items.isDateTime}}localVarJsonArray.push_back( localVarItem.get() ? localVarItem->toJson() : web::json::value::null() ); + {{/items.isDateTime}}{{/items.isString}}{{/items.isPrimitiveType}} + } + localVarJson = web::json::value::array(localVarJsonArray); + } + {{/isArray}} + {{^isArray}}{{#required}}localVarJson = ModelBase::toJson({{paramName}}); + {{/required}}{{^required}}if ({{paramName}}) + localVarJson = ModelBase::toJson(*{{paramName}});{{/required}} + {{/isArray}} + {{/isPrimitiveType}} + + localVarHttpBody = std::shared_ptr<IHttpBody>( new JsonBody( localVarJson ) ); + {{/bodyParam}} + } + // multipart formdata + else if( localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("multipart/form-data")) != localVarConsumeHttpContentTypes.end() ) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("multipart/form-data"); + {{#bodyParam}} + std::shared_ptr<MultipartFormData> localVarMultipart(new MultipartFormData); + {{#isPrimitiveType}} + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}}{{^required}}.get(){{/required}})); + {{/isPrimitiveType}}{{^isPrimitiveType}}{{#isArray}} + { + std::vector<web::json::value> localVarJsonArray; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + localVarJsonArray.push_back(ModelBase::toJson(localVarItem)); + } + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonArray, utility::conversions::to_string_t("application/json"))); + }{{/isArray}}{{#isMap}} + { + std::map<utility::string_t, web::json::value> localVarJsonMap; + for( auto& localVarItem : {{paramName}}{{^required}}.get(){{/required}} ) + { + web::json::value jval; + localVarJsonMap.insert( std::pair<utility::string_t, web::json::value>(localVarItem.first, ModelBase::toJson(localVarItem.second) )); + } + localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), localVarJsonMap, utility::conversions::to_string_t("application/json"))); + }{{/isMap}} + {{^isArray}}{{^isMap}}{{#isString}}localVarMultipart->add(ModelBase::toHttpContent(utility::conversions::to_string_t("{{paramName}}"), {{paramName}})); + {{/isString}}{{^isString}}if({{^required}}{{paramName}} && (*{{paramName}}){{/required}}{{#required}}{{paramName}}{{/required}}.get()) + { + {{^required}}(*{{/required}}{{paramName}}{{^required}}){{/required}}->toMultipart(localVarMultipart, utility::conversions::to_string_t("{{paramName}}")); + } + {{/isString}} + {{/isMap}}{{/isArray}}{{/isPrimitiveType}} + + localVarHttpBody = localVarMultipart; + localVarRequestHttpContentType += utility::conversions::to_string_t("; boundary=") + localVarMultipart->getBoundary(); + {{/bodyParam}} + } + else if (localVarConsumeHttpContentTypes.find(utility::conversions::to_string_t("application/x-www-form-urlencoded")) != localVarConsumeHttpContentTypes.end()) + { + localVarRequestHttpContentType = utility::conversions::to_string_t("application/x-www-form-urlencoded"); + } + else + { + throw ApiException(415, utility::conversions::to_string_t("{{classname}}->{{operationId}} does not consume any supported media type")); + } + + {{#authMethods}} + // authentication ({{name}}) required + {{#isApiKey}} + {{#isKeyInHeader}} + { + utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}")); + if ( localVarApiKey.size() > 0 ) + { + localVarHeaderParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey; + } + } + {{/isKeyInHeader}} + {{#isKeyInQuery}} + { + utility::string_t localVarApiKey = localVarApiConfiguration->getApiKey(utility::conversions::to_string_t("{{keyParamName}}")); + if ( localVarApiKey.size() > 0 ) + { + localVarQueryParams[utility::conversions::to_string_t("{{keyParamName}}")] = localVarApiKey; + } + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{#isBasicBasic}} + // Basic authentication is added automatically as part of the http_client_config + {{/isBasicBasic}} + {{#isOAuth}} + // oauth2 authentication is added automatically as part of the http_client_config + {{/isOAuth}} + {{/authMethods}} + + return m_ApiClient->callApi(localVarPath, utility::conversions::to_string_t("{{httpMethod}}"), localVarQueryParams, localVarHttpBody, localVarHeaderParams, localVarFormParams, localVarFileParams, localVarRequestHttpContentType) + .then([=, this](web::http::http_response localVarResponse) + { + if (m_ApiClient->getResponseHandler()) + { + m_ApiClient->getResponseHandler()(localVarResponse.status_code(), localVarResponse.headers()); + } + + // 1xx - informational : OK + // 2xx - successful : OK + // 3xx - redirection : OK + // 4xx - client error : not OK + // 5xx - client error : not OK + if (localVarResponse.status_code() >= 400) + { + throw ApiException(localVarResponse.status_code() + , utility::conversions::to_string_t("error calling {{operationId}}: ") + localVarResponse.reason_phrase() + , std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get())); + } + + // check response content type + if(localVarResponse.headers().has(utility::conversions::to_string_t("Content-Type"))) + { + utility::string_t localVarContentType = localVarResponse.headers()[utility::conversions::to_string_t("Content-Type")]; + if( localVarContentType.find(localVarResponseHttpContentType) == std::string::npos ) + { + throw ApiException(500 + , utility::conversions::to_string_t("error calling {{operationId}}: unexpected response type: ") + localVarContentType + , std::make_shared<std::stringstream>(localVarResponse.extract_utf8string(true).get())); + } + } + + {{#vendorExtensions.x-codegen-response-ishttpcontent}} + return localVarResponse.extract_vector(); + }) + .then([=, this](std::vector<unsigned char> localVarResponse) + { + {{{returnType}}} localVarResult = std::make_shared<HttpContent>(); + std::shared_ptr<std::stringstream> stream = std::make_shared<std::stringstream>(std::string(localVarResponse.begin(), localVarResponse.end())); + localVarResult->setData(stream); + return localVarResult; + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + {{^vendorExtensions.x-codegen-response-ishttpcontent}} + return localVarResponse.extract_string(); + }) + .then([=, this](utility::string_t localVarResponse) + { + {{^returnType}} + return void(); + {{/returnType}} + {{#returnType}} + {{#returnContainer}} + {{{returnType}}} localVarResult; + {{/returnContainer}} + {{^returnContainer}} + {{{returnType}}} localVarResult({{{defaultResponse}}}); + {{/returnContainer}} + + if(localVarResponseHttpContentType == utility::conversions::to_string_t("application/json")) + { + web::json::value localVarJson = web::json::value::parse(localVarResponse); + {{#isArray}} + for( auto& localVarItem : localVarJson.as_array() ) + { + {{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj; + ModelBase::fromJson(localVarItem, localVarItemObj); + localVarResult.push_back(localVarItemObj); + }{{/isArray}}{{#isMap}} + for( auto& localVarItem : localVarJson.as_object() ) + { + {{{vendorExtensions.x-codegen-response.items.datatype}}} localVarItemObj; + ModelBase::fromJson(localVarItem.second, localVarItemObj); + localVarResult[localVarItem.first] = localVarItemObj; + }{{/isMap}}{{^isArray}}{{^isMap}} + ModelBase::fromJson(localVarJson, localVarResult);{{/isMap}}{{/isArray}} + }{{#vendorExtensions.x-codegen-response.isString}} + else if(localVarResponseHttpContentType == utility::conversions::to_string_t("text/plain")) + { + localVarResult = localVarResponse; + }{{/vendorExtensions.x-codegen-response.isString}} + // else if(localVarResponseHttpContentType == utility::conversions::to_string_t("multipart/form-data")) + // { + // TODO multipart response parsing + // } + else + { + throw ApiException(500 + , utility::conversions::to_string_t("error calling {{operationId}}: unsupported response type")); + } + + return localVarResult; + {{/returnType}} + {{/vendorExtensions.x-codegen-response-ishttpcontent}} + }); +} +{{/operation}} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +{{/operations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiclient-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiclient-header.mustache new file mode 100644 index 0000000..73eee1d --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiclient-header.mustache @@ -0,0 +1,107 @@ +{{>licenseInfo}} +/* + * ApiClient.h + * + * This is an API client responsible for stating the HTTP calls + */ + +#ifndef {{apiHeaderGuardPrefix}}_ApiClient_H_ +#define {{apiHeaderGuardPrefix}}_ApiClient_H_ + +{{{defaultInclude}}} +#include "{{packageName}}/ApiConfiguration.h" +#include "{{packageName}}/ApiException.h" +#include "{{packageName}}/IHttpBody.h" +#include "{{packageName}}/HttpContent.h" +{{^hasModelImport}} +#include "{{packageName}}/ModelBase.h" +{{/hasModelImport}} +#if defined (_WIN32) || defined (_WIN64) +#undef U +#endif + +#include <cpprest/details/basic_types.h> +#include <cpprest/http_client.h> + +#include <memory> +#include <vector> +#include <functional> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +class {{declspec}} ApiClient +{ +public: + ApiClient( std::shared_ptr<const ApiConfiguration> configuration = nullptr ); + virtual ~ApiClient(); + + typedef std::function<void(web::http::status_code, const web::http::http_headers&)> ResponseHandlerType; + + const ResponseHandlerType& getResponseHandler() const; + void setResponseHandler(const ResponseHandlerType& responseHandler); + + std::shared_ptr<const ApiConfiguration> getConfiguration() const; + void setConfiguration(std::shared_ptr<const ApiConfiguration> configuration); + + static utility::string_t parameterToString(utility::string_t value); + static utility::string_t parameterToString(int32_t value); + static utility::string_t parameterToString(int64_t value); + static utility::string_t parameterToString(float value); + static utility::string_t parameterToString(double value); + static utility::string_t parameterToString(const utility::datetime &value); + static utility::string_t parameterToString(bool value); + {{^hasModelImport}} + static utility::string_t parameterToString(const ModelBase& value); + {{/hasModelImport}} + template<class T> + static utility::string_t parameterToString(const std::vector<T>& value); + template<class T> + static utility::string_t parameterToString(const std::shared_ptr<T>& value); + + pplx::task<web::http::http_response> callApi( + const utility::string_t& path, + const utility::string_t& method, + const std::map<utility::string_t, utility::string_t>& queryParams, + const std::shared_ptr<IHttpBody> postBody, + const std::map<utility::string_t, utility::string_t>& headerParams, + const std::map<utility::string_t, utility::string_t>& formParams, + const std::map<utility::string_t, std::shared_ptr<HttpContent>>& fileParams, + const utility::string_t& contentType + ) const; + +protected: + + ResponseHandlerType m_ResponseHandler; + std::shared_ptr<const ApiConfiguration> m_Configuration; +}; + +template<class T> +utility::string_t ApiClient::parameterToString(const std::vector<T>& value) +{ + utility::stringstream_t ss; + + for( size_t i = 0; i < value.size(); i++) + { + if( i > 0) ss << utility::conversions::to_string_t(", "); + ss << ApiClient::parameterToString(value[i]); + } + + return ss.str(); +} + +template<class T> +utility::string_t ApiClient::parameterToString(const std::shared_ptr<T>& value) +{ + return parameterToString(*value.get()); +} + + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif /* {{apiHeaderGuardPrefix}}_ApiClient_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiclient-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiclient-source.mustache new file mode 100644 index 0000000..c69b388 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiclient-source.mustache @@ -0,0 +1,203 @@ +{{>licenseInfo}} +#include "{{packageName}}/ApiClient.h" +#include "{{packageName}}/MultipartFormData.h" +#include "{{packageName}}/ModelBase.h" + +#include <sstream> +#include <limits> +#include <iomanip> + +template <typename T> +utility::string_t toString(const T value) +{ + utility::ostringstream_t out; + out << std::setprecision(std::numeric_limits<T>::digits10) << std::fixed << value; + return out.str(); +} + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +using namespace {{modelNamespace}}; + +ApiClient::ApiClient(std::shared_ptr<const ApiConfiguration> configuration ) + : m_Configuration(configuration) +{ +} +ApiClient::~ApiClient() +{ +} + +const ApiClient::ResponseHandlerType& ApiClient::getResponseHandler() const { + return m_ResponseHandler; +} + +void ApiClient::setResponseHandler(const ResponseHandlerType& responseHandler) { + m_ResponseHandler = responseHandler; +} + +std::shared_ptr<const ApiConfiguration> ApiClient::getConfiguration() const +{ + return m_Configuration; +} +void ApiClient::setConfiguration(std::shared_ptr<const ApiConfiguration> configuration) +{ + m_Configuration = configuration; +} + + +utility::string_t ApiClient::parameterToString(utility::string_t value) +{ + return value; +} +utility::string_t ApiClient::parameterToString(int64_t value) +{ + std::stringstream valueAsStringStream; + valueAsStringStream << value; + return utility::conversions::to_string_t(valueAsStringStream.str()); +} +utility::string_t ApiClient::parameterToString(int32_t value) +{ + std::stringstream valueAsStringStream; + valueAsStringStream << value; + return utility::conversions::to_string_t(valueAsStringStream.str()); +} + +utility::string_t ApiClient::parameterToString(float value) +{ + return utility::conversions::to_string_t(toString(value)); +} + +utility::string_t ApiClient::parameterToString(double value) +{ + return utility::conversions::to_string_t(toString(value)); +} + +utility::string_t ApiClient::parameterToString(const utility::datetime &value) +{ + return utility::conversions::to_string_t(value.to_string(utility::datetime::ISO_8601)); +} + +{{^hasModelImport}} +utility::string_t ApiClient::parameterToString(const ModelBase& value) +{ + return value.toJson().serialize(); +} +{{/hasModelImport}} + +utility::string_t ApiClient::parameterToString(bool value) +{ + std::stringstream valueAsStringStream; + valueAsStringStream << std::boolalpha << value; + return utility::conversions::to_string_t(valueAsStringStream.str()); +} + +pplx::task<web::http::http_response> ApiClient::callApi( + const utility::string_t& path, + const utility::string_t& method, + const std::map<utility::string_t, utility::string_t>& queryParams, + const std::shared_ptr<IHttpBody> postBody, + const std::map<utility::string_t, utility::string_t>& headerParams, + const std::map<utility::string_t, utility::string_t>& formParams, + const std::map<utility::string_t, std::shared_ptr<HttpContent>>& fileParams, + const utility::string_t& contentType +) const +{ + if (postBody != nullptr && formParams.size() != 0) + { + throw ApiException(400, utility::conversions::to_string_t("Cannot have body and form params")); + } + + if (postBody != nullptr && fileParams.size() != 0) + { + throw ApiException(400, utility::conversions::to_string_t("Cannot have body and file params")); + } + + if (fileParams.size() > 0 && contentType != utility::conversions::to_string_t("multipart/form-data")) + { + throw ApiException(400, utility::conversions::to_string_t("Operations with file parameters must be called with multipart/form-data")); + } + + web::http::client::http_client client(m_Configuration->getBaseUrl(), m_Configuration->getHttpConfig()); + + web::http::http_request request; + for (const auto& kvp : headerParams) + { + request.headers().add(kvp.first, kvp.second); + } + + if (fileParams.size() > 0) + { + MultipartFormData uploadData; + for (const auto& kvp : formParams) + { + uploadData.add(ModelBase::toHttpContent(kvp.first, kvp.second)); + } + for (const auto& kvp : fileParams) + { + uploadData.add(ModelBase::toHttpContent(kvp.first, kvp.second)); + } + std::stringstream data; + uploadData.writeTo(data); + auto bodyString = data.str(); + const auto length = bodyString.size(); + request.set_body(concurrency::streams::bytestream::open_istream(std::move(bodyString)), length, utility::conversions::to_string_t("multipart/form-data; boundary=") + uploadData.getBoundary()); + } + else + { + if (postBody != nullptr) + { + std::stringstream data; + postBody->writeTo(data); + auto bodyString = data.str(); + const auto length = bodyString.size(); + request.set_body(concurrency::streams::bytestream::open_istream(std::move(bodyString)), length, contentType); + } + else + { + if (contentType == utility::conversions::to_string_t("application/json")) + { + web::json::value body_data = web::json::value::object(); + for (auto& kvp : formParams) + { + body_data[kvp.first] = ModelBase::toJson(kvp.second); + } + if (!formParams.empty()) + { + request.set_body(body_data); + } + } + else + { + web::http::uri_builder formData; + for (const auto& kvp : formParams) + { + formData.append_query(kvp.first, kvp.second); + } + if (!formParams.empty()) + { + request.set_body(formData.query(), utility::conversions::to_string_t("application/x-www-form-urlencoded")); + } + } + } + } + + web::http::uri_builder builder(path); + for (const auto& kvp : queryParams) + { + builder.append_query(kvp.first, kvp.second); + } + request.set_request_uri(builder.to_uri()); + request.set_method(method); + if ( !request.headers().has( web::http::header_names::user_agent ) ) + { + request.headers().add( web::http::header_names::user_agent, m_Configuration->getUserAgent() ); + } + + return client.request(request); +} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-header.mustache new file mode 100644 index 0000000..1408831 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-header.mustache @@ -0,0 +1,54 @@ +{{>licenseInfo}} +/* + * ApiConfiguration.h + * + * This class represents a single item of a multipart-formdata request. + */ + +#ifndef {{apiHeaderGuardPrefix}}_ApiConfiguration_H_ +#define {{apiHeaderGuardPrefix}}_ApiConfiguration_H_ + +{{{defaultInclude}}} + +#include <cpprest/details/basic_types.h> +#include <cpprest/http_client.h> + +#include <map> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +class {{declspec}} ApiConfiguration +{ +public: + ApiConfiguration(); + virtual ~ApiConfiguration(); + + const web::http::client::http_client_config& getHttpConfig() const; + void setHttpConfig( web::http::client::http_client_config& value ); + + utility::string_t getBaseUrl() const; + void setBaseUrl( const utility::string_t value ); + + utility::string_t getUserAgent() const; + void setUserAgent( const utility::string_t value ); + + std::map<utility::string_t, utility::string_t>& getDefaultHeaders(); + const std::map<utility::string_t, utility::string_t>& getDefaultHeaders() const; + + utility::string_t getApiKey( const utility::string_t& prefix) const; + void setApiKey( const utility::string_t& prefix, const utility::string_t& apiKey ); + +protected: + utility::string_t m_BaseUrl; + std::map<utility::string_t, utility::string_t> m_DefaultHeaders; + std::map<utility::string_t, utility::string_t> m_ApiKeys; + web::http::client::http_client_config m_HttpConfig; + utility::string_t m_UserAgent; +}; + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} +#endif /* {{apiHeaderGuardPrefix}}_ApiConfiguration_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-source.mustache new file mode 100644 index 0000000..365c55c --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiconfiguration-source.mustache @@ -0,0 +1,73 @@ +{{>licenseInfo}} +#include "{{packageName}}/ApiConfiguration.h" + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +ApiConfiguration::ApiConfiguration() +{ +} + +ApiConfiguration::~ApiConfiguration() +{ +} + +const web::http::client::http_client_config& ApiConfiguration::getHttpConfig() const +{ + return m_HttpConfig; +} + +void ApiConfiguration::setHttpConfig( web::http::client::http_client_config& value ) +{ + m_HttpConfig = value; +} + +utility::string_t ApiConfiguration::getBaseUrl() const +{ + return m_BaseUrl; +} + +void ApiConfiguration::setBaseUrl( const utility::string_t value ) +{ + m_BaseUrl = value; +} + +utility::string_t ApiConfiguration::getUserAgent() const +{ + return m_UserAgent; +} + +void ApiConfiguration::setUserAgent( const utility::string_t value ) +{ + m_UserAgent = value; +} + +std::map<utility::string_t, utility::string_t>& ApiConfiguration::getDefaultHeaders() +{ + return m_DefaultHeaders; +} + +const std::map<utility::string_t, utility::string_t>& ApiConfiguration::getDefaultHeaders() const +{ + return m_DefaultHeaders; +} + +utility::string_t ApiConfiguration::getApiKey( const utility::string_t& prefix) const +{ + auto result = m_ApiKeys.find(prefix); + if( result != m_ApiKeys.end() ) + { + return result->second; + } + return utility::conversions::to_string_t(""); +} + +void ApiConfiguration::setApiKey( const utility::string_t& prefix, const utility::string_t& apiKey ) +{ + m_ApiKeys[prefix] = apiKey; +} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiexception-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiexception-header.mustache new file mode 100644 index 0000000..5383a2d --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiexception-header.mustache @@ -0,0 +1,48 @@ +{{>licenseInfo}} +/* + * ApiException.h + * + * This is the exception being thrown in case the api call was not successful + */ + +#ifndef {{apiHeaderGuardPrefix}}_ApiException_H_ +#define {{apiHeaderGuardPrefix}}_ApiException_H_ + +{{{defaultInclude}}} + +#include <cpprest/details/basic_types.h> +#include <cpprest/http_msg.h> + +#include <memory> +#include <map> + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +class {{declspec}} ApiException + : public web::http::http_exception +{ +public: + ApiException( int errorCode + , const utility::string_t& message + , std::shared_ptr<std::istream> content = nullptr ); + ApiException( int errorCode + , const utility::string_t& message + , std::map<utility::string_t, utility::string_t>& headers + , std::shared_ptr<std::istream> content = nullptr ); + virtual ~ApiException(); + + std::map<utility::string_t, utility::string_t>& getHeaders(); + std::shared_ptr<std::istream> getContent() const; + +protected: + std::shared_ptr<std::istream> m_Content; + std::map<utility::string_t, utility::string_t> m_Headers; +}; + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} + +#endif /* {{apiHeaderGuardPrefix}}_ApiBase_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/apiexception-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/apiexception-source.mustache new file mode 100644 index 0000000..3ca444e --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/apiexception-source.mustache @@ -0,0 +1,41 @@ +{{>licenseInfo}} +#include "{{packageName}}/ApiException.h" + +{{#apiNamespaceDeclarations}} +namespace {{this}} { +{{/apiNamespaceDeclarations}} + +ApiException::ApiException( int errorCode + , const utility::string_t& message + , std::shared_ptr<std::istream> content /*= nullptr*/ ) + : web::http::http_exception( errorCode, message ) + , m_Content(content) +{ +} +ApiException::ApiException( int errorCode + , const utility::string_t& message + , std::map<utility::string_t, utility::string_t>& headers + , std::shared_ptr<std::istream> content /*= nullptr*/ ) + : web::http::http_exception( errorCode, message ) + , m_Content(content) + , m_Headers(headers) +{ +} + +ApiException::~ApiException() +{ +} + +std::shared_ptr<std::istream> ApiException::getContent() const +{ + return m_Content; +} + +std::map<utility::string_t, utility::string_t>& ApiException::getHeaders() +{ + return m_Headers; +} + +{{#apiNamespaceDeclarations}} +} +{{/apiNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/cmake-config.mustache b/api/_build/my-cpp-templates/cpp-restsdk/cmake-config.mustache new file mode 100644 index 0000000..9015c2b --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/cmake-config.mustache @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +include(${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake) + +check_required_components("@PROJECT_NAME@") diff --git a/api/_build/my-cpp-templates/cpp-restsdk/cmake-lists.mustache b/api/_build/my-cpp-templates/cpp-restsdk/cmake-lists.mustache new file mode 100644 index 0000000..7b7225c --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/cmake-lists.mustache @@ -0,0 +1,91 @@ +# +# {{{appName}}} +# {{{appDescription}}} +# +# The version of the OpenAPI document: 1.0.0 +# +# https://openapi-generator.tech +# +# NOTE: Auto generated by OpenAPI Generator (https://openapi-generator.tech). + +cmake_minimum_required (VERSION 3.10) + +project({{{packageName}}} CXX) + +# Force -fPIC even if the project is configured for building a static library. +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(CXX_STANDARD_REQUIRED ON) +if(NOT CMAKE_CXX_STANDARD) + if(DEFINED CMAKE_CXX20_STANDARD_COMPILE_OPTION OR + DEFINED CMAKE_CXX20_EXTENSION_COMPILE_OPTION) + set(CMAKE_CXX_STANDARD 20) + elseif(DEFINED CMAKE_CXX17_STANDARD_COMPILE_OPTION OR + DEFINED CMAKE_CXX17_EXTENSION_COMPILE_OPTION) + set(CMAKE_CXX_STANDARD 17) + elseif(DEFINED CMAKE_CXX14_STANDARD_COMPILE_OPTION OR + DEFINED CMAKE_CXX14_EXTENSION_COMPILE_OPTION) + set(CMAKE_CXX_STANDARD 14) + else() + set(CMAKE_CXX_STANDARD 11) + endif() +endif() + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +find_package(cpprestsdk REQUIRED) +target_compile_definitions(cpprestsdk::cpprest INTERFACE _TURN_OFF_PLATFORM_STRING) +find_package(Boost REQUIRED) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +file(GLOB_RECURSE HEADER_FILES "include/*.h") +file(GLOB_RECURSE SOURCE_FILES "src/*.cpp") + +add_library(${PROJECT_NAME} ${HEADER_FILES} ${SOURCE_FILES}) + +target_compile_options(${PROJECT_NAME} + PRIVATE + $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>: + -Wall -Wno-unused-variable -Wno-unused-lambda-capture> +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> + $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> +) + +target_link_libraries(${PROJECT_NAME} PUBLIC Boost::headers cpprestsdk::cpprest) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" +) + +install( + TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/${PROJECT_NAME} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} +) + +install( + EXPORT ${PROJECT_NAME}Targets + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} +) diff --git a/api/_build/my-cpp-templates/cpp-restsdk/git_push.sh.mustache b/api/_build/my-cpp-templates/cpp-restsdk/git_push.sh.mustache new file mode 100644 index 0000000..0e3776a --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/git_push.sh.mustache @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="{{{gitHost}}}" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="{{{gitUserId}}}" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="{{{gitRepoId}}}" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="{{{releaseNote}}}" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/api/_build/my-cpp-templates/cpp-restsdk/gitignore.mustache b/api/_build/my-cpp-templates/cpp-restsdk/gitignore.mustache new file mode 100644 index 0000000..4581ef2 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/gitignore.mustache @@ -0,0 +1,29 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-header.mustache new file mode 100644 index 0000000..cae6a0e --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-header.mustache @@ -0,0 +1,57 @@ +{{>licenseInfo}} +/* + * HttpContent.h + * + * This class represents a single item of a multipart-formdata request. + */ + +#ifndef {{modelHeaderGuardPrefix}}_HttpContent_H_ +#define {{modelHeaderGuardPrefix}}_HttpContent_H_ + +{{{defaultInclude}}} + +#include <cpprest/details/basic_types.h> + +#include <memory> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} HttpContent +{ +public: + HttpContent(); + virtual ~HttpContent(); + + virtual utility::string_t getContentDisposition() const; + virtual void setContentDisposition( const utility::string_t& value ); + + virtual utility::string_t getName() const; + virtual void setName( const utility::string_t& value ); + + virtual utility::string_t getFileName() const; + virtual void setFileName( const utility::string_t& value ); + + virtual utility::string_t getContentType() const; + virtual void setContentType( const utility::string_t& value ); + + virtual std::shared_ptr<std::istream> getData() const; + virtual void setData( std::shared_ptr<std::istream> value ); + + virtual void writeTo( std::ostream& stream ); + +protected: + // NOTE: no utility::string_t here because those strings can only contain ascii + utility::string_t m_ContentDisposition; + utility::string_t m_Name; + utility::string_t m_FileName; + utility::string_t m_ContentType; + std::shared_ptr<std::istream> m_Data; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_HttpContent_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-source.mustache new file mode 100644 index 0000000..f9739c5 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/httpcontent-source.mustache @@ -0,0 +1,74 @@ +{{>licenseInfo}} +#include "{{packageName}}/HttpContent.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +HttpContent::HttpContent() +{ +} + +HttpContent::~HttpContent() +{ +} + +utility::string_t HttpContent::getContentDisposition() const +{ + return m_ContentDisposition; +} + +void HttpContent::setContentDisposition( const utility::string_t & value ) +{ + m_ContentDisposition = value; +} + +utility::string_t HttpContent::getName() const +{ + return m_Name; +} + +void HttpContent::setName( const utility::string_t & value ) +{ + m_Name = value; +} + +utility::string_t HttpContent::getFileName() const +{ + return m_FileName; +} + +void HttpContent::setFileName( const utility::string_t & value ) +{ + m_FileName = value; +} + +utility::string_t HttpContent::getContentType() const +{ + return m_ContentType; +} + +void HttpContent::setContentType( const utility::string_t & value ) +{ + m_ContentType = value; +} + +std::shared_ptr<std::istream> HttpContent::getData() const +{ + return m_Data; +} + +void HttpContent::setData( std::shared_ptr<std::istream> value ) +{ + m_Data = value; +} + +void HttpContent::writeTo( std::ostream& stream ) +{ + m_Data->seekg( 0, m_Data->beg ); + stream << m_Data->rdbuf(); +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/ihttpbody-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/ihttpbody-header.mustache new file mode 100644 index 0000000..e9c77b2 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/ihttpbody-header.mustache @@ -0,0 +1,30 @@ +{{>licenseInfo}} +/* + * IHttpBody.h + * + * This is the interface for contents that can be sent to a remote HTTP server. + */ + +#ifndef {{modelHeaderGuardPrefix}}_IHttpBody_H_ +#define {{modelHeaderGuardPrefix}}_IHttpBody_H_ + +{{{defaultInclude}}} +#include <iostream> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} IHttpBody +{ +public: + virtual ~IHttpBody() { } + + virtual void writeTo( std::ostream& stream ) = 0; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_IHttpBody_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-header.mustache new file mode 100644 index 0000000..7c6ac16 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-header.mustache @@ -0,0 +1,37 @@ +{{>licenseInfo}} +/* + * JsonBody.h + * + * This is a JSON http body which can be submitted via http + */ + +#ifndef {{modelHeaderGuardPrefix}}_JsonBody_H_ +#define {{modelHeaderGuardPrefix}}_JsonBody_H_ + +{{{defaultInclude}}} +#include "{{packageName}}/IHttpBody.h" + +#include <cpprest/json.h> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} JsonBody + : public IHttpBody +{ +public: + JsonBody( const web::json::value& value ); + virtual ~JsonBody(); + + void writeTo( std::ostream& target ) override; + +protected: + web::json::value m_Json; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_JsonBody_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-source.mustache new file mode 100644 index 0000000..4129111 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/jsonbody-source.mustache @@ -0,0 +1,24 @@ +{{>licenseInfo}} +#include "{{packageName}}/JsonBody.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +JsonBody::JsonBody( const web::json::value& json) + : m_Json(json) +{ +} + +JsonBody::~JsonBody() +{ +} + +void JsonBody::writeTo( std::ostream& target ) +{ + m_Json.serialize(target); +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/licenseInfo.mustache b/api/_build/my-cpp-templates/cpp-restsdk/licenseInfo.mustache new file mode 100644 index 0000000..4b9c44b --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/licenseInfo.mustache @@ -0,0 +1,15 @@ +/** + * {{{appName}}} + * {{{appDescription}}} + * + {{#version}} + * The version of the OpenAPI document: {{{.}}} + {{/version}} + {{#infoEmail}} + * Contact: {{{.}}} + {{/infoEmail}} + * + * NOTE: This class is auto generated by OpenAPI-Generator {{{generatorVersion}}}. + * https://openapi-generator.tech + * Do not edit the class manually. + */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/model-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/model-header.mustache new file mode 100644 index 0000000..825b73a --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/model-header.mustache @@ -0,0 +1,288 @@ +{{>licenseInfo}} +{{#models}}{{#model}}/* + * {{classname}}.h + * + * {{description}} + */ + +#ifndef {{modelHeaderGuardPrefix}}_{{classname}}_H_ +#define {{modelHeaderGuardPrefix}}_{{classname}}_H_ + +{{#hasEnums}} +#include <stdexcept> +{{/hasEnums}} +{{#oneOf}} +{{#-first}} +#include <variant> +{{/-first}} +{{/oneOf}} +{{^parent}} +{{{defaultInclude}}} +#include "{{packageName}}/ModelBase.h" +{{/parent}} + +{{#imports}}{{{this}}} +{{/imports}} + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +{{#vendorExtensions.x-has-forward-declarations}} +{{#vendorExtensions.x-forward-declarations}}{{.}} +{{/vendorExtensions.x-forward-declarations}} +{{/vendorExtensions.x-has-forward-declarations}} +{{#oneOf}}{{#-first}} + +class {{declspec}} {{classname}} +{ +public: + {{classname}}() = default; + ~{{classname}}() = default; + + ///////////////////////////////////////////// + + void validate(); + + web::json::value toJson() const; + + template<typename Target> + bool fromJson(const web::json::value& json) { + // convert json to Target type + Target target; + if (!target.fromJson(json)) { + return false; + } + + m_variantValue = target; + return true; + } + + void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const; + + template<typename Target> + bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) { + // convert multipart to Target type + Target target; + if (!target.fromMultiPart(multipart, namePrefix)) { + return false; + } + + m_variantValue = target; + return true; + } + + ///////////////////////////////////////////// + /// {{classname}} members + + using VariantType = std::variant<{{#oneOf}}{{^-first}}, {{/-first}}{{{.}}}{{/oneOf}}>; + + const VariantType& getVariant() const; + void setVariant(VariantType value); + +protected: + VariantType m_variantValue; +}; + +{{/-first}}{{/oneOf}} +{{^oneOf}} +{{#isEnum}} +class {{declspec}} {{classname}} + : public {{{parent}}}{{^parent}}ModelBase{{/parent}} +{ +public: + {{classname}}(); + {{classname}}(utility::string_t str); + operator utility::string_t() const { + return enumToStrMap.at(getValue()); + } + + {{! operator std::string() const { + return enumToStrMap.at(getValue()); + } }} + + virtual ~{{classname}}(); + + ///////////////////////////////////////////// + /// ModelBase overrides + + void validate() override; + + web::json::value toJson() const override; + bool fromJson(const web::json::value& json) override; + + void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override; + bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override; + + enum class e{{classname}} + { + {{#allowableValues}} + {{#enumVars}} + {{#enumDescription}} + /// <summary> + /// {{.}} + /// </summary> + {{/enumDescription}} + {{{name}}}{{^last}},{{/last}} + {{/enumVars}} + {{/allowableValues}} + }; + + e{{classname}} getValue() const; + void setValue(e{{classname}} const value); + + protected: + e{{classname}} m_value; + std::map<e{{classname}},utility::string_t> enumToStrMap = { + {{#allowableValues}} + {{#enumVars}} + { e{{classname}}::{{{name}}}, _XPLATSTR("{{{name}}}") }{{^-last}},{{/-last}} + {{/enumVars}} + {{/allowableValues}} +}; + std::map<utility::string_t,e{{classname}}> strToEnumMap = { + {{#allowableValues}} + {{#enumVars}} + { _XPLATSTR("{{{name}}}"), e{{classname}}::{{{name}}} }{{^-last}},{{/-last}} + {{/enumVars}} + {{/allowableValues}} +}; + +}; +{{/isEnum}} +{{^isEnum}} + +{{#description}} +/// <summary> +/// {{description}} +/// </summary> +{{/description}} +class {{declspec}} {{classname}} + : public {{{parent}}}{{^parent}}ModelBase{{/parent}} +{ +public: + {{classname}}(); + virtual ~{{classname}}(); + + ///////////////////////////////////////////// + /// ModelBase overrides + + void validate() override; + + web::json::value toJson() const override; + bool fromJson(const web::json::value& json) override; + + void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override; + bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override; + + + ///////////////////////////////////////////// + /// {{classname}} members + + {{! ENUM DEFINITIONS }} + {{#vars}} + {{^isInherited}} + {{#isEnum}} + enum class {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}} + { + {{#allowableValues}} + {{#enumVars}} + {{{name}}}{{^last}},{{/last}} + {{/enumVars}} + {{/allowableValues}} + }; + {{#description}} + /// <summary> + /// {{description}} + /// </summary> + {{/description}} + {{/isEnum}} + {{/isInherited}} + {{/vars}} + {{#vars}} + {{^isInherited}} + {{#isEnum}} + {{#isContainer}} + {{! ENUM CONVERSIONS }} + {{{enumName}}} to{{{enumName}}}(const utility::string_t& value) const; + const utility::string_t from{{{enumName}}}(const {{{enumName}}} value) const; + {{#isArray}} + {{{datatypeWithEnum}}} to{{{enumName}}}(const {{{dataType}}}& value) const; + {{{dataType}}} from{{{enumName}}}(const {{{datatypeWithEnum}}}& value) const; + {{/isArray}}{{/isContainer}}{{^isContainer}} + {{{datatypeWithEnum}}} to{{{datatypeWithEnum}}}(const utility::string_t& value) const; + const utility::string_t from{{{datatypeWithEnum}}}(const {{{datatypeWithEnum}}} value) const; + {{/isContainer}} + + {{/isEnum}} + {{/isInherited}} + {{/vars}} + + {{! SETTER AND GETTERS }} + {{#vars}} + {{^isInherited}} + {{#description}} + /// <summary> + /// {{description}} + /// </summary> + {{/description}} + {{#isContainer}} + {{^isEnum}} + {{{dataType}}} {{getter}}() const; + {{/isEnum}} + {{/isContainer}} + {{^isContainer}} + {{^isEnum}} + {{{dataType}}} {{getter}}() const; + {{/isEnum}} + {{/isContainer}} + {{#isEnum}} + {{^isMap}} + {{{datatypeWithEnum}}} {{getter}}() const; + {{/isMap}} + {{#isMap}} + {{{dataType}}} {{getter}}() const; + {{/isMap}} + {{/isEnum}} + bool {{nameInCamelCase}}IsSet() const; + void unset{{name}}(); + {{#isPrimitiveType}} + void {{setter}}({{{dataType}}} value); + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{^isEnum}} + void {{setter}}(const {{{dataType}}}& value); + {{/isEnum}} + {{/isPrimitiveType}} + {{#isEnum}} + void {{setter}}(const {{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} value); + {{/isEnum}} + {{/isInherited}} + + {{/vars}} + +protected: + {{#vars}} + {{^isInherited}} + {{^isEnum}} + {{{dataType}}} m_{{name}}; + {{/isEnum}} + {{#isEnum}} + {{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} m_{{name}}; + {{/isEnum}} + bool m_{{name}}IsSet; + {{/isInherited}} + + {{/vars}} +}; + +{{/isEnum}} +{{/oneOf}} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_{{classname}}_H_ */ +{{/model}} +{{/models}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/model-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/model-source.mustache new file mode 100644 index 0000000..635cf43 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/model-source.mustache @@ -0,0 +1,457 @@ +{{>licenseInfo}} +{{#models}}{{#model}} + +#include "{{packageName}}/model/{{classFilename}}.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} +{{#oneOf}} +{{#-first}} + +void {{classname}}::validate() +{ + // TODO: implement validation +} + +const {{classname}}::VariantType& {{classname}}::getVariant() const +{ + return m_variantValue; +} + +void {{classname}}::setVariant({{classname}}::VariantType value) +{ + m_variantValue = value; +} + +web::json::value {{classname}}::toJson() const +{ + web::json::value val = web::json::value::object(); + + std::visit([&](auto&& arg) { + using T = std::decay_t<decltype(arg)>; + if constexpr (std::is_same_v<T, std::monostate>) { + val = web::json::value::null(); + } else { + val = arg.toJson(); + } + }, m_variantValue); + + return val; +} + +void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const +{ + std::visit([&](auto&& arg) { + using T = std::decay_t<decltype(arg)>; + if constexpr (!std::is_same_v<T, std::monostate>) { + arg.toMultipart(multipart, prefix); + } + }, m_variantValue); +} + +{{#oneOf}} +template bool {{classname}}::fromJson<{{.}}>(const web::json::value& json); +template bool {{classname}}::fromMultiPart<{{.}}>(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix); +{{/oneOf}} + +{{/-first}} +{{/oneOf}} +{{^oneOf}} +{{#isEnum}} + +namespace +{ +using EnumUnderlyingType = {{#isNumeric}}int64_t{{/isNumeric}}{{^isNumeric}}utility::string_t{{/isNumeric}}; + +{{classname}}::e{{classname}} toEnum(const EnumUnderlyingType& val) +{ +{{#allowableValues}} +{{#isNumeric}} + switch (val) + { + {{#enumVars}} + case {{value}}: + return {{classname}}::e{{classname}}::{{name}}; + {{#-last}} + default: + break; + {{/-last}} + {{/enumVars}} + } +{{/isNumeric}} +{{^isNumeric}} + {{#enumVars}} + if (val == utility::conversions::to_string_t(_XPLATSTR("{{{value}}}"))) + return {{classname}}::e{{classname}}::{{name}}; + {{/enumVars}} +{{/isNumeric}} +{{/allowableValues}} + return {}; +} + +EnumUnderlyingType fromEnum({{classname}}::e{{classname}} e) +{ +{{#allowableValues}} + switch (e) + { +{{#enumVars}} + case {{classname}}::e{{classname}}::{{name}}: + return {{#isNumeric}}{{value}}{{/isNumeric}}{{^isNumeric}}_XPLATSTR("{{value}}"){{/isNumeric}}; +{{#-last}} + default: + break; +{{/-last}} +{{/enumVars}} + } +{{/allowableValues}} + return {}; +} +} + +{{classname}}::{{classname}}() +{ +} + +{{classname}}::~{{classname}}() +{ +} + +void {{classname}}::validate() +{ + // TODO: implement validation +} + +web::json::value {{classname}}::toJson() const +{ + auto val = fromEnum(m_value); + return web::json::value(val); +} + +bool {{classname}}::fromJson(const web::json::value& val) +{ + m_value = toEnum({{#isNumeric}}val.as_number().to_int64(){{/isNumeric}}{{^isNumeric}}val.as_string(){{/isNumeric}}); + return true; +} + +void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const +{ + utility::string_t namePrefix = prefix; + if (!namePrefix.empty() && namePrefix.back() != _XPLATSTR('.')) + { + namePrefix.push_back(_XPLATSTR('.')); + } + + auto e = fromEnum(m_value); + multipart->add(ModelBase::toHttpContent(namePrefix, e)); +} + +bool {{classname}}::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) +{ + bool ok = true; + utility::string_t namePrefix = prefix; + if (!namePrefix.empty() && namePrefix.back() != _XPLATSTR('.')) + { + namePrefix.push_back(_XPLATSTR('.')); + } + { + EnumUnderlyingType e; + ok = ModelBase::fromHttpContent(multipart->getContent(namePrefix), e); + if (ok) + { + auto v = toEnum(e); + setValue(v); + } + } + return ok; +} + +{{classname}}::e{{classname}} {{classname}}::getValue() const +{ + return m_value; +} + +void {{classname}}::setValue({{classname}}::e{{classname}} const value) +{ + m_value = value; +} + +{{classname}}::{{classname}}(utility::string_t str){ + setValue( strToEnumMap[str] ); +} + +{{/isEnum}} +{{^isEnum}} + +{{classname}}::{{classname}}() +{ + {{#vars}} + {{^isInherited}} + {{^isContainer}} + {{^isEnum}} + {{#isPrimitiveType}} + m_{{name}} = {{{defaultValue}}}; + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isString}} + m_{{name}} = {{{defaultValue}}}; + {{/isString}} + {{#isDateTime}} + m_{{name}} = {{{defaultValue}}}; + {{/isDateTime}} + {{/isPrimitiveType}} + {{/isEnum}} + {{/isContainer}} + m_{{name}}IsSet = false; + {{/isInherited}} + {{/vars}} +} + +{{classname}}::~{{classname}}() +{ +} + +void {{classname}}::validate() +{ + // TODO: implement validation +} + +web::json::value {{classname}}::toJson() const +{ + {{#parent}} + web::json::value val = this->{{{.}}}::toJson(); + {{/parent}} + {{^parent}} + web::json::value val = web::json::value::object(); + {{/parent}} + {{#vars}} + {{^isInherited}} + if(m_{{name}}IsSet) + { + {{#isEnum}}{{#isContainer}}{{#isArray}} + {{{dataType}}} refVal = from{{{enumName}}}(m_{{name}}); + {{/isArray}}{{#isMap}} + val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(m_{{name}}); + {{/isMap}}{{/isContainer}}{{^isContainer}} + utility::string_t refVal = from{{{datatypeWithEnum}}}(m_{{name}}); + {{/isContainer}}{{^isMap}}val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(refVal); + {{/isMap}}{{/isEnum}} + {{^isEnum}} + val[utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))] = ModelBase::toJson(m_{{name}}); + {{/isEnum}} + } + {{/isInherited}} + {{/vars}} + + return val; +} + +bool {{classname}}::fromJson(const web::json::value& val) +{ + bool ok = true; + {{#parent}} + ok &= this->{{{.}}}::fromJson(val); + {{/parent}} + {{#vars}} + {{^isInherited}} + if(val.has_field(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")))) + { + const web::json::value& fieldValue = val.at(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))); + if(!fieldValue.is_null()) + { + {{{dataType}}} refVal_{{setter}}; + ok &= ModelBase::fromJson(fieldValue, refVal_{{setter}}); + {{^isEnum}} + {{setter}}(refVal_{{setter}}); + {{/isEnum}} + {{#isEnum}}{{#isContainer}}{{#isArray}} + {{setter}}(to{{{enumName}}}(refVal_{{setter}})); + {{/isArray}}{{#isMap}} + {{setter}}(refVal_{{setter}}); + {{/isMap}}{{/isContainer}}{{^isContainer}} + {{setter}}(to{{{datatypeWithEnum}}}(refVal_{{setter}})); + {{/isContainer}}{{/isEnum}} + } + } + {{/isInherited}} + {{/vars}} + return ok; +} + +void {{classname}}::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const +{ + utility::string_t namePrefix = prefix; + if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(_XPLATSTR("."))) + { + namePrefix += utility::conversions::to_string_t(_XPLATSTR(".")); + } + {{#vars}} + if(m_{{name}}IsSet) + { + {{^isEnum}} + multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), m_{{name}})); + {{/isEnum}} + {{#isEnum}} + {{#isContainer}} + {{#isArray}} + multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), from{{{enumName}}}(m_{{name}}))); + {{/isArray}}{{#isMap}} + multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), m_{{name}})); + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")), from{{{datatypeWithEnum}}}(m_{{name}}))); + {{/isContainer}} + {{/isEnum}} + } + {{/vars}} +} + +bool {{classname}}::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) +{ + bool ok = true; + utility::string_t namePrefix = prefix; + if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(_XPLATSTR("."))) + { + namePrefix += utility::conversions::to_string_t(_XPLATSTR(".")); + } + + {{#vars}} + if(multipart->hasContent(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}")))) + { + {{{dataType}}} refVal_{{setter}}; + ok &= ModelBase::fromHttpContent(multipart->getContent(utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))), refVal_{{setter}} ); + {{^isEnum}} + {{setter}}(refVal_{{setter}}); + {{/isEnum}} + {{#isEnum}} + {{#isContainer}} + {{#isArray}} + {{setter}}(to{{{enumName}}}(refVal_{{setter}})); + {{/isArray}} + {{#isMap}} + {{setter}}(refVal_{{setter}}); + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + {{setter}}(to{{{datatypeWithEnum}}}(refVal_{{setter}})); + {{/isContainer}} + {{/isEnum}} + } + {{/vars}} + return ok; +} + +{{#vars}} +{{^isInherited}} +{{#isEnum}} +{{#isContainer}} +{{classname}}::{{{enumName}}} {{classname}}::to{{{enumName}}}(const utility::string_t& value) const +{{/isContainer}} +{{^isContainer}} +{{classname}}::{{{datatypeWithEnum}}} {{classname}}::to{{{datatypeWithEnum}}}(const {{dataType}}& value) const +{{/isContainer}} +{ + {{#allowableValues}}{{#enumVars}} + if (value == utility::conversions::to_string_t("{{value}}")) { + return {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}::{{name}}; + } + {{/enumVars}}{{/allowableValues}} + throw std::invalid_argument("Invalid value for conversion to {{{datatypeWithEnum}}}"); +} + +{{#isContainer}} +const utility::string_t {{classname}}::from{{{enumName}}}(const {{{enumName}}} value) const +{{/isContainer}}{{^isContainer}} +const {{dataType}} {{classname}}::from{{{datatypeWithEnum}}}(const {{{datatypeWithEnum}}} value) const +{{/isContainer}} +{ + switch(value) + { + {{#allowableValues}}{{#enumVars}} + case {{#isContainer}}{{{enumName}}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}::{{name}}: return utility::conversions::to_string_t("{{value}}"); + {{/enumVars}}{{/allowableValues}} + } +} + +{{#isContainer}} +{{#isArray}} +{{{dataType}}} {{{classname}}}::from{{{enumName}}}(const {{{datatypeWithEnum}}}& value) const +{ + {{{dataType}}} ret; + for (auto it = value.begin(); it != value.end(); it++) { + ret.push_back(from{{{enumName}}}(*it)); + } + return ret; +} + +{{{baseType}}}<{{classname}}::{{{enumName}}}> {{{classname}}}::to{{{enumName}}}(const {{{dataType}}}& value) const +{ + {{{datatypeWithEnum}}} ret; + for (auto it = value.begin(); it != value.end(); it++) { + ret.push_back(to{{{enumName}}}(*it)); + } + return ret; +} +{{/isArray}} +{{/isContainer}} +{{/isEnum}} +{{/isInherited}} +{{/vars}} + +{{#vars}} +{{^isInherited}} +{{#isContainer}} +{{^isEnum}} +{{{dataType}}} {{classname}}::{{getter}}() const +{ + return m_{{name}}; +} +{{/isEnum}} +{{/isContainer}} +{{^isContainer}} +{{^isEnum}} +{{{dataType}}} {{classname}}::{{getter}}() const +{ + return m_{{name}}; +} +{{/isEnum}} +{{/isContainer}} +{{#isEnum}} +{{^isMap}}{{#isArray}}{{{baseType}}}<{{/isArray}}{{{classname}}}::{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} {{classname}}::{{getter}}() const +{ + return m_{{name}}; +} +{{/isEnum}} + +{{#isPrimitiveType}} +void {{classname}}::{{setter}}({{{dataType}}} value) +{{/isPrimitiveType}}{{^isPrimitiveType}}{{^isEnum}} +void {{classname}}::{{setter}}(const {{{dataType}}}& value) +{{/isEnum}}{{/isPrimitiveType}}{{#isEnum}} +void {{classname}}::{{setter}}(const {{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{#isMap}}{{{dataType}}}{{/isMap}} value) +{{/isEnum}} +{ + m_{{name}} = value; + m_{{name}}IsSet = true; +} + +bool {{classname}}::{{nameInCamelCase}}IsSet() const +{ + return m_{{name}}IsSet; +} + +void {{classname}}::unset{{name}}() +{ + m_{{name}}IsSet = false; +} +{{/isInherited}}{{/vars}} +{{/isEnum}} +{{/oneOf}} +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + + +{{/model}} +{{/models}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/modelbase-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/modelbase-header.mustache new file mode 100644 index 0000000..b97a7ad --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/modelbase-header.mustache @@ -0,0 +1,493 @@ +{{>licenseInfo}} +/* + * ModelBase.h + * + * This is the base class for all model classes + */ + +#ifndef {{modelHeaderGuardPrefix}}_ModelBase_H_ +#define {{modelHeaderGuardPrefix}}_ModelBase_H_ + +{{{defaultInclude}}} + +#include "{{packageName}}/HttpContent.h" +#include "{{packageName}}/MultipartFormData.h" + +#include <cpprest/details/basic_types.h> +#include <cpprest/json.h> + +#include <map> +#include <set> +#include <vector> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} ModelBase +{ +public: + ModelBase(); + virtual ~ModelBase(); + + virtual void validate() = 0; + + virtual web::json::value toJson() const = 0; + virtual bool fromJson( const web::json::value& json ) = 0; + + virtual void toMultipart( std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix ) const = 0; + virtual bool fromMultiPart( std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix ) = 0; + + virtual bool isSet() const; + + static utility::string_t toString( const bool val ); + static utility::string_t toString( const float val ); + static utility::string_t toString( const double val ); + static utility::string_t toString( const int32_t val ); + static utility::string_t toString( const int64_t val ); + static utility::string_t toString( const utility::string_t &val ); + static utility::string_t toString( const utility::datetime &val ); + static utility::string_t toString( const web::json::value &val ); + static utility::string_t toString( const std::shared_ptr<HttpContent>& val ); + template <typename T> + static utility::string_t toString( const std::shared_ptr<T>& val ); + template <typename T> + static utility::string_t toString( const std::vector<T> & val ); + template <typename T> + static utility::string_t toString( const std::set<T> & val ); + + static web::json::value toJson( bool val ); + static web::json::value toJson( float val ); + static web::json::value toJson( double val ); + static web::json::value toJson( int32_t val ); + static web::json::value toJson( int64_t val ); + static web::json::value toJson( const utility::string_t& val ); + static web::json::value toJson( const utility::datetime& val ); + static web::json::value toJson( const web::json::value& val ); + static web::json::value toJson( const std::shared_ptr<HttpContent>& val ); + template<typename T> + static web::json::value toJson( const std::shared_ptr<T>& val ); + static web::json::value toJson( const std::shared_ptr<utility::datetime>& val ); + template<typename T> + static web::json::value toJson( const std::vector<T>& val ); + template<typename T> + static web::json::value toJson( const std::set<T>& val ); + template<typename T> + static web::json::value toJson( const std::map<utility::string_t, T>& val ); + + static bool fromString( const utility::string_t& val, bool & ); + static bool fromString( const utility::string_t& val, float & ); + static bool fromString( const utility::string_t& val, double & ); + static bool fromString( const utility::string_t& val, int32_t & ); + static bool fromString( const utility::string_t& val, int64_t & ); + static bool fromString( const utility::string_t& val, utility::string_t & ); + static bool fromString( const utility::string_t& val, utility::datetime & ); + static bool fromString( const utility::string_t& val, web::json::value & ); + static bool fromString( const utility::string_t& val, std::shared_ptr<HttpContent> & ); + template<typename T> + static bool fromString( const utility::string_t& val, std::shared_ptr<T>& ); + static bool fromString( const utility::string_t& val, std::shared_ptr<utility::datetime>& outVal ); + template<typename T> + static bool fromString( const utility::string_t& val, std::vector<T> & ); + template<typename T> + static bool fromString( const utility::string_t& val, std::set<T> & ); + template<typename T> + static bool fromString( const utility::string_t& val, std::map<utility::string_t, T> & ); + + static bool fromJson( const web::json::value& val, bool & ); + static bool fromJson( const web::json::value& val, float & ); + static bool fromJson( const web::json::value& val, double & ); + static bool fromJson( const web::json::value& val, int32_t & ); + static bool fromJson( const web::json::value& val, int64_t & ); + static bool fromJson( const web::json::value& val, utility::string_t & ); + static bool fromJson( const web::json::value& val, utility::datetime & ); + static bool fromJson( const web::json::value& val, web::json::value & ); + static bool fromJson( const web::json::value& val, std::shared_ptr<HttpContent> & ); + template<typename T> + static bool fromJson( const web::json::value& val, std::shared_ptr<T>& ); + static bool fromJson( const web::json::value& val, std::shared_ptr<utility::datetime> &outVal ); + template<typename T> + static bool fromJson( const web::json::value& val, std::vector<T> & ); + template<typename T> + static bool fromJson( const web::json::value& val, std::set<T> & ); + template<typename T> + static bool fromJson( const web::json::value& val, std::map<utility::string_t, T> & ); + + + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, bool value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, float value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, double value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, int32_t value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, int64_t value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const utility::string_t& value, const utility::string_t& contentType = utility::conversions::to_string_t("")); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const utility::datetime& value, const utility::string_t& contentType = utility::conversions::to_string_t("")); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const web::json::value& value, const utility::string_t& contentType = utility::conversions::to_string_t("application/json") ); + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::shared_ptr<HttpContent>& ); + template <typename T> + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::shared_ptr<T>& , const utility::string_t& contentType = utility::conversions::to_string_t("application/json") ); + static std::shared_ptr<HttpContent> toHttpContent(const utility::string_t& name, const std::shared_ptr<utility::datetime>& value , const utility::string_t& contentType = utility::conversions::to_string_t("application/json") ); + template <typename T> + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::vector<T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + template <typename T> + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::set<T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + template <typename T> + static std::shared_ptr<HttpContent> toHttpContent( const utility::string_t& name, const std::map<utility::string_t, T>& value, const utility::string_t& contentType = utility::conversions::to_string_t("") ); + + static bool fromHttpContent( std::shared_ptr<HttpContent> val, bool & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, float & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, double & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, int64_t & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, int32_t & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, utility::string_t & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, utility::datetime & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, web::json::value & ); + static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<HttpContent>& ); + template <typename T> + static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<T>& ); + template <typename T> + static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::vector<T> & ); + template <typename T> + static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::set<T> & ); + template <typename T> + static bool fromHttpContent( std::shared_ptr<HttpContent> val, std::map<utility::string_t, T> & ); + + static utility::string_t toBase64( utility::string_t value ); + static utility::string_t toBase64( std::shared_ptr<std::istream> value ); + static std::shared_ptr<std::istream> fromBase64( const utility::string_t& encoded ); +protected: + bool m_IsSet; +}; + +template <typename T> +utility::string_t ModelBase::toString( const std::shared_ptr<T>& val ) +{ + utility::stringstream_t ss; + if( val != nullptr ) + { + val->toJson().serialize(ss); + } + return utility::string_t(ss.str()); +} + +// std::vector to string +template<typename T> +utility::string_t ModelBase::toString( const std::vector<T> & val ) +{ + utility::string_t strArray; + for ( const auto &item : val ) + { + strArray.append( toString(item) + "," ); + } + if (val.count() > 0) + { + strArray.pop_back(); + } + return strArray; +} + +// std::set to string +template<typename T> +utility::string_t ModelBase::toString( const std::set<T> & val ) +{ + utility::string_t strArray; + for ( const auto &item : val ) + { + strArray.append( toString(item) + "," ); + } + if (val.count() > 0) + { + strArray.pop_back(); + } + return strArray; +} + + +template<typename T> +web::json::value ModelBase::toJson( const std::shared_ptr<T>& val ) +{ + web::json::value retVal; + if(val != nullptr) + { + retVal = val->toJson(); + } + return retVal; +} + +// std::vector to json +template<typename T> +web::json::value ModelBase::toJson( const std::vector<T>& value ) +{ + std::vector<web::json::value> ret; + for ( const auto& x : value ) + { + ret.push_back( toJson(x) ); + } + return web::json::value::array(ret); +} + +// std::set to json +template<typename T> +web::json::value ModelBase::toJson( const std::set<T>& value ) +{ + // There's no prototype web::json::value::array(...) taking a std::set parameter. Converting to std::vector to get an array. + std::vector<web::json::value> ret; + for ( const auto& x : value ) + { + ret.push_back( toJson(x) ); + } + return web::json::value::array(ret); +} + + +template<typename T> +web::json::value ModelBase::toJson( const std::map<utility::string_t, T>& val ) +{ + web::json::value obj; + for ( const auto &itemkey : val ) + { + obj[itemkey.first] = toJson( itemkey.second ); + } + return obj; +} +template<typename T> +bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<T>& outVal ) +{ + bool ok = false; + if(outVal == nullptr) + { + outVal = std::make_shared<T>(); + } + if( outVal != nullptr ) + { + ok = outVal->fromJson(web::json::value::parse(val)); + } + return ok; +} +template<typename T> +bool ModelBase::fromString(const utility::string_t& val, std::vector<T>& outVal ) +{ + bool ok = true; + web::json::value jsonValue = web::json::value::parse(val); + if (jsonValue.is_array()) + { + for (const web::json::value& jitem : jsonValue.as_array()) + { + T item; + ok &= fromJson(jitem, item); + outVal.push_back(item); + } + } + else + { + T item; + ok = fromJson(jsonValue, item); + outVal.push_back(item); + } + return ok; +} +template<typename T> +bool ModelBase::fromString(const utility::string_t& val, std::set<T>& outVal ) +{ + bool ok = true; + web::json::value jsonValue = web::json::value::parse(val); + if (jsonValue.is_array()) + { + for (const web::json::value& jitem : jsonValue.as_array()) + { + T item; + ok &= fromJson(jitem, item); + outVal.insert(item); + } + } + else + { + T item; + ok = fromJson(jsonValue, item); + outVal.insert(item); + } + return ok; +} +template<typename T> +bool ModelBase::fromString(const utility::string_t& val, std::map<utility::string_t, T>& outVal ) +{ + bool ok = false; + web::json::value jsonValue = web::json::value::parse(val); + if (jsonValue.is_array()) + { + for (const web::json::value& jitem : jsonValue.as_array()) + { + T item; + ok &= fromJson(jitem, item); + outVal.insert({ val, item }); + } + } + else + { + T item; + ok = fromJson(jsonValue, item); + outVal.insert({ val, item }); + } + return ok; +} +template<typename T> +bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<T> &outVal ) +{ + bool ok = false; + if(outVal == nullptr) + { + outVal = std::make_shared<T>(); + } + if( outVal != nullptr ) + { + ok = outVal->fromJson(val); + } + return ok; +} +template<typename T> +bool ModelBase::fromJson( const web::json::value& val, std::vector<T> &outVal ) +{ + bool ok = true; + if (val.is_array()) + { + for (const web::json::value & jitem : val.as_array()) + { + T item; + ok &= fromJson(jitem, item); + outVal.push_back(item); + } + } + else + { + ok = false; + } + return ok; +} +template<typename T> +bool ModelBase::fromJson(const web::json::value& val, std::set<T>& outVal ) +{ + bool ok = true; + if (val.is_array()) + { + for (const web::json::value& jitem : val.as_array()) + { + T item; + ok &= fromJson(jitem, item); + outVal.insert(item); + } + } + else + { + T item; + ok = fromJson(val, item); + outVal.insert(item); + } + return ok; +} +template<typename T> +bool ModelBase::fromJson( const web::json::value& jval, std::map<utility::string_t, T> &outVal ) +{ + bool ok = true; + if ( jval.is_object() ) + { + auto obj = jval.as_object(); + for( auto objItr = obj.begin() ; objItr != obj.end() ; objItr++ ) + { + T itemVal; + ok &= fromJson(objItr->second, itemVal); + outVal.insert(std::pair<utility::string_t, T>(objItr->first, itemVal)); + } + } + else + { + ok = false; + } + return ok; +} +template <typename T> +std::shared_ptr<HttpContent> ModelBase::toHttpContent(const utility::string_t& name, const std::shared_ptr<T>& value , const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>(); + if (value != nullptr ) + { + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(value->toJson().serialize()) ) ); + } + return content; +} + +template <typename T> +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::vector<T>& value, const utility::string_t& contentType ) +{ + web::json::value json_array = ModelBase::toJson(value); + std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>(); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(json_array.serialize()) ) ); + return content; +} +template <typename T> +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::set<T>& value, const utility::string_t& contentType ) +{ + web::json::value json_array = ModelBase::toJson(value); + std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>(); + content->setName(name); + content->setContentDisposition(utility::conversions::to_string_t("form-data")); + content->setContentType(contentType); + content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(json_array.serialize()) ) ); + return content; +} +template <typename T> +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::map<utility::string_t, T>& value, const utility::string_t& contentType ) +{ + web::json::value jobj = ModelBase::toJson(value); + std::shared_ptr<HttpContent> content = std::make_shared<HttpContent>(); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::make_shared<std::stringstream>( utility::conversions::to_utf8string(jobj.serialize()) ) ); + return content; +} +template <typename T> +bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::shared_ptr<T>& outVal ) +{ + utility::string_t str; + if(val == nullptr) return false; + if( outVal == nullptr ) + { + outVal = std::make_shared<T>(); + } + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +template <typename T> +bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::vector<T> & outVal ) +{ + utility::string_t str; + if (val == nullptr) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +template <typename T> +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, std::set<T>& outVal ) +{ + utility::string_t str; + if (val == nullptr) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +template <typename T> +bool ModelBase::fromHttpContent( std::shared_ptr<HttpContent> val, std::map<utility::string_t, T> & outVal ) +{ + utility::string_t str; + if (val == nullptr) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_ModelBase_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/modelbase-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/modelbase-source.mustache new file mode 100644 index 0000000..7528343 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/modelbase-source.mustache @@ -0,0 +1,653 @@ +{{>licenseInfo}} +#include "{{packageName}}/ModelBase.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +ModelBase::ModelBase(): m_IsSet(false) +{ +} +ModelBase::~ModelBase() +{ +} +bool ModelBase::isSet() const +{ + return m_IsSet; +} +utility::string_t ModelBase::toString( const bool val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString( const float val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString( const double val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString( const int32_t val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString( const int64_t val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString (const utility::string_t &val ) +{ + utility::stringstream_t ss; + ss << val; + return utility::string_t(ss.str()); +} +utility::string_t ModelBase::toString( const utility::datetime &val ) +{ + return val.to_string(utility::datetime::ISO_8601); +} +utility::string_t ModelBase::toString( const web::json::value &val ) +{ + return val.serialize(); +} +utility::string_t ModelBase::toString( const std::shared_ptr<HttpContent>& val ) +{ + utility::stringstream_t ss; + if( val != nullptr ) + { + ss << val->getData(); + } + return utility::string_t(ss.str()); +} +web::json::value ModelBase::toJson(bool value) +{ + return web::json::value::boolean(value); +} +web::json::value ModelBase::toJson( float value ) +{ + return web::json::value::number(value); +} +web::json::value ModelBase::toJson( double value ) +{ + return web::json::value::number(value); +} +web::json::value ModelBase::toJson( int32_t value ) +{ + return web::json::value::number(value); +} +web::json::value ModelBase::toJson( int64_t value ) +{ + return web::json::value::number(value); +} +web::json::value ModelBase::toJson( const utility::string_t& value ) +{ + return web::json::value::string(value); +} +web::json::value ModelBase::toJson( const utility::datetime& value ) +{ + return web::json::value::string(value.to_string(utility::datetime::ISO_8601)); +} +web::json::value ModelBase::toJson( const web::json::value& value ) +{ + return value; +} +web::json::value ModelBase::toJson( const std::shared_ptr<HttpContent>& content ) +{ + web::json::value value; + if(content != nullptr) + { + value[utility::conversions::to_string_t("ContentDisposition")] = ModelBase::toJson(content->getContentDisposition()); + value[utility::conversions::to_string_t("ContentType")] = ModelBase::toJson(content->getContentType()); + value[utility::conversions::to_string_t("FileName")] = ModelBase::toJson(content->getFileName()); + value[utility::conversions::to_string_t("InputStream")] = web::json::value::string( ModelBase::toBase64(content->getData()) ); + } + return value; +} +web::json::value ModelBase::toJson( const std::shared_ptr<utility::datetime>& val ) +{ + web::json::value retVal; + if(val != nullptr) + { + retVal = toJson(*val); + } + return retVal; +} +bool ModelBase::fromString( const utility::string_t& val, bool &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + success = false; + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, float &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + int64_t intVal = 0; + success = ModelBase::fromString(val, intVal); + if(success) + { + outVal = static_cast<float>(intVal); + } + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, double &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + int64_t intVal = 0; + success = ModelBase::fromString(val, intVal); + if(success) + { + outVal = static_cast<double>(intVal); + } + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, int32_t &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + success = false; + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, int64_t &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + success = false; + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, utility::string_t &outVal ) +{ + utility::stringstream_t ss(val); + bool success = true; + try + { + ss >> outVal; + } + catch (...) + { + success = false; + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, utility::datetime &outVal ) +{ + bool success = true; + auto dt = utility::datetime::from_string(val, utility::datetime::ISO_8601); + if( dt.is_initialized() ) + { + outVal = dt; + } + else + { + success = false; + } + return success; +} +bool ModelBase::fromString( const utility::string_t& val, web::json::value &outVal ) +{ + outVal = web::json::value::parse(val); + return !outVal.is_null(); +} +bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<HttpContent>& outVal ) +{ + bool ok = true; + if(outVal == nullptr) + { + outVal = std::shared_ptr<HttpContent>(new HttpContent()); + } + if(outVal != nullptr) + { + outVal->setData(std::shared_ptr<std::istream>(new std::stringstream(utility::conversions::to_utf8string(val)))); + } + else + { + ok = false; + } + return ok; +} +bool ModelBase::fromString( const utility::string_t& val, std::shared_ptr<utility::datetime>& outVal ) +{ + bool ok = false; + if(outVal == nullptr) + { + outVal = std::shared_ptr<utility::datetime>(new utility::datetime()); + } + if( outVal != nullptr ) + { + ok = fromJson(web::json::value::parse(val), *outVal); + } + return ok; +} +bool ModelBase::fromJson( const web::json::value& val, bool & outVal ) +{ + outVal = !val.is_boolean() ? false : val.as_bool(); + return val.is_boolean(); +} +bool ModelBase::fromJson( const web::json::value& val, float & outVal ) +{ + outVal = (!val.is_double() && !val.is_integer()) ? std::numeric_limits<float>::quiet_NaN(): static_cast<float>(val.as_double()); + return val.is_double() || val.is_integer(); +} +bool ModelBase::fromJson( const web::json::value& val, double & outVal ) +{ + outVal = (!val.is_double() && !val.is_integer()) ? std::numeric_limits<double>::quiet_NaN(): val.as_double(); + return val.is_double() || val.is_integer(); +} +bool ModelBase::fromJson( const web::json::value& val, int32_t & outVal ) +{ + outVal = !val.is_integer() ? std::numeric_limits<int32_t>::quiet_NaN() : val.as_integer(); + return val.is_integer(); +} +bool ModelBase::fromJson( const web::json::value& val, int64_t & outVal ) +{ + outVal = !val.is_number() ? std::numeric_limits<int64_t>::quiet_NaN() : val.as_number().to_int64(); + return val.is_number(); +} +bool ModelBase::fromJson( const web::json::value& val, utility::string_t & outVal ) +{ + outVal = val.is_string() ? val.as_string() : utility::conversions::to_string_t(""); + return val.is_string(); +} +bool ModelBase::fromJson( const web::json::value& val, utility::datetime & outVal ) +{ + outVal = val.is_null() ? utility::datetime::from_string(utility::conversions::to_string_t("NULL"), utility::datetime::ISO_8601) : utility::datetime::from_string(val.as_string(), utility::datetime::ISO_8601); + return outVal.is_initialized(); +} +bool ModelBase::fromJson( const web::json::value& val, web::json::value & outVal ) +{ + outVal = val; + return !val.is_null(); +} +bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<HttpContent>& content ) +{ + bool result = false; + if( content != nullptr) + { + result = true; + if(content == nullptr) + { + content = std::shared_ptr<HttpContent>(new HttpContent()); + } + if(val.has_field(utility::conversions::to_string_t("ContentDisposition"))) + { + utility::string_t value; + result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("ContentDisposition")), value); + content->setContentDisposition( value ); + } + if(val.has_field(utility::conversions::to_string_t("ContentType"))) + { + utility::string_t value; + result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("ContentType")), value); + content->setContentType( value ); + } + if(val.has_field(utility::conversions::to_string_t("FileName"))) + { + utility::string_t value; + result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("FileName")), value); + content->setFileName( value ); + } + if(val.has_field(utility::conversions::to_string_t("InputStream"))) + { + utility::string_t value; + result = result && ModelBase::fromJson(val.at(utility::conversions::to_string_t("InputStream")), value); + content->setData( ModelBase::fromBase64( value ) ); + } + } + return result; +} +bool ModelBase::fromJson( const web::json::value& val, std::shared_ptr<utility::datetime> &outVal ) +{ + bool ok = false; + if(outVal == nullptr) + { + outVal = std::shared_ptr<utility::datetime>(new utility::datetime()); + } + if( outVal != nullptr ) + { + ok = fromJson(val, *outVal); + } + return ok; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, bool value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + std::stringstream* valueAsStringStream = new std::stringstream(); + (*valueAsStringStream) << value; + content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, float value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + std::stringstream* valueAsStringStream = new std::stringstream(); + (*valueAsStringStream) << value; + content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, double value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + std::stringstream* valueAsStringStream = new std::stringstream(); + (*valueAsStringStream) << value; + content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, int32_t value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + std::stringstream* valueAsStringStream = new std::stringstream(); + (*valueAsStringStream) << value; + content->setData( std::shared_ptr<std::istream>( valueAsStringStream ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, int64_t value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + std::stringstream* valueAsStringStream = new std::stringstream(); + (*valueAsStringStream) << value; + content->setData( std::shared_ptr<std::istream>( valueAsStringStream) ) ; + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const utility::string_t& value, const utility::string_t& contentType) +{ + std::shared_ptr<HttpContent> content(new HttpContent); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value) ) ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const utility::datetime& value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value.to_string(utility::datetime::ISO_8601) ) ) ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const web::json::value& value, const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string(value.serialize()) ) ) ); + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent( const utility::string_t& name, const std::shared_ptr<HttpContent>& value ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + if( value != nullptr ) + { + content->setName( name ); + content->setContentDisposition( value->getContentDisposition() ); + content->setContentType( value->getContentType() ); + content->setData( value->getData() ); + content->setFileName( value->getFileName() ); + } + return content; +} +std::shared_ptr<HttpContent> ModelBase::toHttpContent(const utility::string_t& name, const std::shared_ptr<utility::datetime>& value , const utility::string_t& contentType ) +{ + std::shared_ptr<HttpContent> content( new HttpContent ); + if (value != nullptr ) + { + content->setName( name ); + content->setContentDisposition( utility::conversions::to_string_t("form-data") ); + content->setContentType( contentType ); + content->setData( std::shared_ptr<std::istream>( new std::stringstream( utility::conversions::to_utf8string( toJson(*value).serialize() ) ) ) ); + } + return content; +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, bool & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, float & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, double & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, int32_t & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, int64_t & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, utility::string_t & outVal ) +{ + if( val == nullptr ) return false; + std::shared_ptr<std::istream> data = val->getData(); + data->seekg( 0, data->beg ); + + std::string str((std::istreambuf_iterator<char>(*data.get())), + std::istreambuf_iterator<char>()); + outVal = utility::conversions::to_string_t(str); + return true; +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, utility::datetime & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + outVal = utility::datetime::from_string(str, utility::datetime::ISO_8601); + return true; +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, web::json::value & outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +bool ModelBase::fromHttpContent(std::shared_ptr<HttpContent> val, std::shared_ptr<HttpContent>& outVal ) +{ + utility::string_t str; + if( val == nullptr ) return false; + if( outVal == nullptr ) + { + outVal = std::shared_ptr<HttpContent>(new HttpContent()); + } + ModelBase::fromHttpContent(val, str); + return fromString(str, outVal); +} +// base64 encoding/decoding based on : https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64#C.2B.2B +const static char Base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const static char Base64PadChar = '='; +utility::string_t ModelBase::toBase64( utility::string_t value ) +{ + std::shared_ptr<std::istream> source( new std::stringstream( utility::conversions::to_utf8string(value) ) ); + return ModelBase::toBase64(source); +} +utility::string_t ModelBase::toBase64( std::shared_ptr<std::istream> value ) +{ + value->seekg( 0, value->end ); + size_t length = value->tellg(); + value->seekg( 0, value->beg ); + utility::string_t base64; + base64.reserve( ((length / 3) + (length % 3 > 0)) * 4 ); + char read[3] = { 0 }; + uint32_t temp; + for ( size_t idx = 0; idx < length / 3; idx++ ) + { + value->read( read, 3 ); + temp = (read[0]) << 16; + temp += (read[1]) << 8; + temp += (read[2]); + base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] ); + base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] ); + base64.append( 1, Base64Chars[(temp & 0x00000FC0) >> 6] ); + base64.append( 1, Base64Chars[(temp & 0x0000003F)] ); + } + switch ( length % 3 ) + { + case 1: + value->read( read, 1 ); + temp = read[0] << 16; + base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] ); + base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] ); + base64.append( 2, Base64PadChar ); + break; + case 2: + value->read( read, 2 ); + temp = read[0] << 16; + temp += read[1] << 8; + base64.append( 1, Base64Chars[(temp & 0x00FC0000) >> 18] ); + base64.append( 1, Base64Chars[(temp & 0x0003F000) >> 12] ); + base64.append( 1, Base64Chars[(temp & 0x00000FC0) >> 6] ); + base64.append( 1, Base64PadChar ); + break; + } + return base64; +} +std::shared_ptr<std::istream> ModelBase::fromBase64( const utility::string_t& encoded ) +{ + std::shared_ptr<std::stringstream> result(new std::stringstream); + + char outBuf[3] = { 0 }; + uint32_t temp = 0; + + utility::string_t::const_iterator cursor = encoded.begin(); + while ( cursor < encoded.end() ) + { + for ( size_t quantumPosition = 0; quantumPosition < 4; quantumPosition++ ) + { + temp <<= 6; + if ( *cursor >= 0x41 && *cursor <= 0x5A ) + { + temp |= *cursor - 0x41; + } + else if ( *cursor >= 0x61 && *cursor <= 0x7A ) + { + temp |= *cursor - 0x47; + } + else if ( *cursor >= 0x30 && *cursor <= 0x39 ) + { + temp |= *cursor + 0x04; + } + else if ( *cursor == 0x2B ) + { + temp |= 0x3E; //change to 0x2D for URL alphabet + } + else if ( *cursor == 0x2F ) + { + temp |= 0x3F; //change to 0x5F for URL alphabet + } + else if ( *cursor == Base64PadChar ) //pad + { + switch ( encoded.end() - cursor ) + { + case 1: //One pad character + outBuf[0] = (temp >> 16) & 0x000000FF; + outBuf[1] = (temp >> 8) & 0x000000FF; + result->write( outBuf, 2 ); + return result; + case 2: //Two pad characters + outBuf[0] = (temp >> 10) & 0x000000FF; + result->write( outBuf, 1 ); + return result; + default: + throw web::json::json_exception( utility::conversions::to_string_t( "Invalid Padding in Base 64!" ).c_str() ); + } + } + else + { + throw web::json::json_exception( utility::conversions::to_string_t( "Non-Valid Character in Base 64!" ).c_str() ); + } + ++cursor; + } + + outBuf[0] = (temp >> 16) & 0x000000FF; + outBuf[1] = (temp >> 8) & 0x000000FF; + outBuf[2] = (temp) & 0x000000FF; + result->write( outBuf, 3 ); + } + + return result; +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/multipart-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/multipart-header.mustache new file mode 100644 index 0000000..7430f4e --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/multipart-header.mustache @@ -0,0 +1,49 @@ +{{>licenseInfo}} +/* + * MultipartFormData.h + * + * This class represents a container for building application/x-multipart-formdata requests. + */ + +#ifndef {{modelHeaderGuardPrefix}}_MultipartFormData_H_ +#define {{modelHeaderGuardPrefix}}_MultipartFormData_H_ + +{{{defaultInclude}}} +#include "{{packageName}}/IHttpBody.h" +#include "{{packageName}}/HttpContent.h" + +#include <cpprest/details/basic_types.h> + +#include <vector> +#include <map> +#include <memory> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} MultipartFormData + : public IHttpBody +{ +public: + MultipartFormData(); + MultipartFormData(const utility::string_t& boundary); + virtual ~MultipartFormData(); + + virtual void add( std::shared_ptr<HttpContent> content ); + virtual utility::string_t getBoundary(); + virtual std::shared_ptr<HttpContent> getContent(const utility::string_t& name) const; + virtual bool hasContent(const utility::string_t& name) const; + virtual void writeTo( std::ostream& target ); + +protected: + std::vector<std::shared_ptr<HttpContent>> m_Contents; + utility::string_t m_Boundary; + std::map<utility::string_t, std::shared_ptr<HttpContent>> m_ContentLookup; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_MultipartFormData_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/multipart-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/multipart-source.mustache new file mode 100644 index 0000000..1b5c083 --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/multipart-source.mustache @@ -0,0 +1,100 @@ +{{>licenseInfo}} +#include "{{packageName}}/MultipartFormData.h" +#include "{{packageName}}/ModelBase.h" + +#include <boost/uuid/random_generator.hpp> +#include <boost/uuid/uuid_io.hpp> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +MultipartFormData::MultipartFormData() +{ + utility::stringstream_t uuidString; + uuidString << boost::uuids::random_generator()(); + m_Boundary = uuidString.str(); +} + +MultipartFormData::MultipartFormData(const utility::string_t& boundary) + : m_Boundary(boundary) +{ + +} + +MultipartFormData::~MultipartFormData() +{ +} + +utility::string_t MultipartFormData::getBoundary() +{ + return m_Boundary; +} + +void MultipartFormData::add( std::shared_ptr<HttpContent> content ) +{ + m_Contents.push_back( content ); + m_ContentLookup[content->getName()] = content; +} + +bool MultipartFormData::hasContent(const utility::string_t& name) const +{ + return m_ContentLookup.find(name) != m_ContentLookup.end(); +} + +std::shared_ptr<HttpContent> MultipartFormData::getContent(const utility::string_t& name) const +{ + auto result = m_ContentLookup.find(name); + if(result == m_ContentLookup.end()) + { + return std::shared_ptr<HttpContent>(nullptr); + } + return result->second; +} + +void MultipartFormData::writeTo( std::ostream& target ) +{ + for ( size_t i = 0; i < m_Contents.size(); i++ ) + { + std::shared_ptr<HttpContent> content = m_Contents[i]; + + // boundary + target << "\r\n" << "--" << utility::conversions::to_utf8string( m_Boundary ) << "\r\n"; + + // headers + target << "Content-Disposition: " << utility::conversions::to_utf8string( content->getContentDisposition() ); + if ( content->getName().size() > 0 ) + { + target << "; name=\"" << utility::conversions::to_utf8string( content->getName() ) << "\""; + } + if ( content->getFileName().size() > 0 ) + { + target << "; filename=\"" << utility::conversions::to_utf8string( content->getFileName() ) << "\""; + } + target << "\r\n"; + + if ( content->getContentType().size() > 0 ) + { + target << "Content-Type: " << utility::conversions::to_utf8string( content->getContentType() ) << "\r\n"; + } + + target << "\r\n"; + + // body + std::shared_ptr<std::istream> data = content->getData(); + + data->seekg( 0, data->end ); + std::vector<char> dataBytes( data->tellg() ); + + data->seekg( 0, data->beg ); + data->read( &dataBytes[0], dataBytes.size() ); + + std::copy( dataBytes.begin(), dataBytes.end(), std::ostreambuf_iterator<char>( target ) ); + } + + target << "\r\n--" << utility::conversions::to_utf8string( m_Boundary ) << "--\r\n"; +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} diff --git a/api/_build/my-cpp-templates/cpp-restsdk/object-header.mustache b/api/_build/my-cpp-templates/cpp-restsdk/object-header.mustache new file mode 100644 index 0000000..3597e1b --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/object-header.mustache @@ -0,0 +1,50 @@ +{{>licenseInfo}} +/* + * Object.h + * + * This is the implementation of a JSON object. + */ + +#ifndef {{modelHeaderGuardPrefix}}_Object_H_ +#define {{modelHeaderGuardPrefix}}_Object_H_ + +{{{defaultInclude}}} +#include "{{packageName}}/ModelBase.h" + +#include <cpprest/details/basic_types.h> +#include <cpprest/json.h> + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +class {{declspec}} Object : public ModelBase +{ +public: + Object(); + virtual ~Object(); + + ///////////////////////////////////////////// + /// ModelBase overrides + void validate() override; + + web::json::value toJson() const override; + bool fromJson(const web::json::value& json) override; + + void toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) const override; + bool fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& namePrefix) override; + + ///////////////////////////////////////////// + /// Object manipulation + web::json::value getValue(const utility::string_t& key) const; + void setValue(const utility::string_t& key, const web::json::value& value); + +private: + web::json::value m_object; +}; + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} + +#endif /* {{modelHeaderGuardPrefix}}_Object_H_ */ diff --git a/api/_build/my-cpp-templates/cpp-restsdk/object-source.mustache b/api/_build/my-cpp-templates/cpp-restsdk/object-source.mustache new file mode 100644 index 0000000..fe9c93b --- /dev/null +++ b/api/_build/my-cpp-templates/cpp-restsdk/object-source.mustache @@ -0,0 +1,79 @@ +{{>licenseInfo}} +#include "{{packageName}}/Object.h" + +{{#modelNamespaceDeclarations}} +namespace {{this}} { +{{/modelNamespaceDeclarations}} + +Object::Object() +{ + m_object = web::json::value::object(); +} + +Object::~Object() +{ +} + +void Object::validate() +{ + +} + +web::json::value Object::toJson() const +{ + return m_object; +} + +bool Object::fromJson(const web::json::value& val) +{ + if (val.is_object()) + { + m_object = val; + m_IsSet = true; + } + return isSet(); +} + +void Object::toMultipart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) const +{ + utility::string_t namePrefix = prefix; + if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(".")) + { + namePrefix += utility::conversions::to_string_t("."); + } + multipart->add(ModelBase::toHttpContent(namePrefix + utility::conversions::to_string_t("object"), m_object)); +} + +bool Object::fromMultiPart(std::shared_ptr<MultipartFormData> multipart, const utility::string_t& prefix) +{ + utility::string_t namePrefix = prefix; + if(namePrefix.size() > 0 && namePrefix.substr(namePrefix.size() - 1) != utility::conversions::to_string_t(".")) + { + namePrefix += utility::conversions::to_string_t("."); + } + + if( ModelBase::fromHttpContent(multipart->getContent(namePrefix + utility::conversions::to_string_t("object")), m_object ) ) + { + m_IsSet = true; + } + return isSet(); +} + +web::json::value Object::getValue(const utility::string_t& key) const +{ + return m_object.at(key); +} + + +void Object::setValue(const utility::string_t& key, const web::json::value& value) +{ + if( !value.is_null() ) + { + m_object[key] = value; + m_IsSet = true; + } +} + +{{#modelNamespaceDeclarations}} +} +{{/modelNamespaceDeclarations}} From a22c96e7a021d10b80bf7d5f08a4823025e2e4a2 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 5 Dec 2025 23:27:02 +0300 Subject: [PATCH 24/33] build(tgbot-gen): fixing a generator error with a tag The generator makes an error when generating tags with a link to the tag. At the moment, this is fixed by direct insertion. This is not the best solution. If a better option is found, we will fix it. --- api/_build/openapi.yaml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index e096beb..d7a6fac 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -550,20 +550,13 @@ components: required: - id - name - Tag: - description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' - type: object - example: - en: Shojo - ru: Сёдзё - ja: 少女 - additionalProperties: - type: string Tags: description: Array of localized tags type: array items: - $ref: '#/components/schemas/Tag' + type: object + additionalProperties: + type: string example: - en: Shojo ru: Сёдзё From 19164b8d9dc9e72585250df09a2bd51e44723ee4 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 5 Dec 2025 23:42:30 +0300 Subject: [PATCH 25/33] refactor(tgbot-front): moved the navigation handler to a separate file --- modules/bot/front/include/handlers.hpp | 2 +- modules/bot/front/src/handleNavigation.cpp | 147 +++++++++++++++++++++ modules/bot/front/src/handlers.cpp | 146 +------------------- 3 files changed, 150 insertions(+), 145 deletions(-) create mode 100644 modules/bot/front/src/handleNavigation.cpp diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index cb462cd..873d74c 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -71,7 +71,7 @@ private: /// @param userId Идентификатор пользователя /// @param payload Полезная нагрузка /// @return HandlerResult - static HandlerResult returnMyTitles(int64_t userId, int64_t payload); + /// static HandlerResult returnMyTitles(int64_t userId, int64_t payload); /// @brief Вход в новое состояние /// @param ctx текущий контекст diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp new file mode 100644 index 0000000..0bd02a3 --- /dev/null +++ b/modules/bot/front/src/handleNavigation.cpp @@ -0,0 +1,147 @@ +#include "handlers.hpp" +#include "KeyboardFactory.hpp" +#include "structs.hpp" +#include "constants.hpp" + +void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { + const auto& current = ctx.history.back(); // текущий экран + const std::string& data = query->data; + + // Пагинация (в списках) + if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT) + && (current.state == UserState::VIEWING_MY_TITLES || current.state == UserState::VIEWING_REVIEW_LIST || + current.state == UserState::VIEWING_FOUND_TITLES)) { + + int64_t newPayload = current.payload; + if (data == BotConstants::Callback::LIST_PREV && newPayload > 0) { + reducePayload(newPayload, current.state); + } else if (data == BotConstants::Callback::LIST_NEXT) { + increasePayload(newPayload, current.state); + } else { + if (data == BotConstants::Callback::LIST_PREV) { + std::cout << "Error: navigation:prev callback for 1st page" << std::endl; + return; + } + // TODO: log + std::cout << "Error: navigation:prev unknown error" << std::endl; + } + + ctx.history.back().payload = newPayload; + + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать + editMessage(query, result); + return; + } + + // Обработка back по интерфейсу + if (data == BotConstants::Callback::NAV_BACK) { + if (!popState(ctx)) { + sendError(query, BotConstants::Text::SAD_ERROR); + return; + } + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать + editMessage(query, result); + return; + } + + // Переходы вперёд (push) + auto newStepOpt = computeNextStep(query, current); + if (!newStepOpt.has_value()) { + sendError(query, BotConstants::Text::SAD_ERROR); + return; + } + + ctx.history.push_back(*newStepOpt); + auto result = renderCurrent(query, ctx); + if(result.message == "meow") return; // TODO: убрать + editMessage(query, result); +} + +HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { + const auto& step = ctx.history.back(); + int64_t userId = query->from->id; + switch (step.state) { + case UserState::MAIN_MENU: + return showMainMenu(); + case UserState::VIEWING_MY_TITLES: + server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId + .then([this, query](pplx::task<std::vector<BotStructs::Title>> t) { + try { + auto titles = t.get(); + + std::string message = formatTitlesList(titles); + auto keyboard = KeyboardFactory::createMyTitles(titles); + + editMessage(query, {message, keyboard}); + + } catch (const std::exception& e) { + sendError(query, BotConstants::Text::SERVER_ERROR); + // Логирование ошибки (например, в cerr) + } + }); + + return {"meow", nullptr}; + /* + case UserState::VIEWING_TITLE_PAGE: + return returnTitlePage(step.payload); // payload = titleId + case UserState::VIEWING_REVIEW: + return returnReview(step.payload); // payload = reviewId + case UserState::AWAITING_REVIEW: + return HandlerResult{"Пришлите текст отзыва:", nullptr}; + // ... + */ + default: + return HandlerResult{BotConstants::Text::SAD_ERROR, nullptr}; + } +} + +std::optional<NavigationStep> BotHandlers::computeNextStep( + const TgBot::CallbackQuery::Ptr& query, + const NavigationStep& current +) { + const std::string& data = query->data; + + switch (current.state) { + case UserState::MAIN_MENU: + if (data == BotConstants::Callback::MY_TITLES) { + return NavigationStep{UserState::VIEWING_MY_TITLES, 0}; + } + break; + /* + case UserState::VIEWING_MY_TITLES: + if (data.starts_with("title_")) { + int64_t titleId = parseId(data); + return NavigationStep{UserState::VIEWING_TITLE_PAGE, titleId}; + } + break; + + case UserState::VIEWING_TITLE_PAGE: + if (data == BotConstants::Callback::ACTION_ADD_REVIEW) { + return NavigationStep{UserState::AWAITING_REVIEW, current.payload}; + } + if (data.starts_with("review_")) { + int64_t reviewId = parseId(data); + return NavigationStep{UserState::VIEWING_REVIEW, reviewId}; + } + break; + */ + default: + break; + } + return std::nullopt; +} + +std::string BotHandlers::formatTitlesList(const std::vector<BotStructs::Title>& titles) { + if (titles.empty()) { + return "У вас пока нет тайтлов."; + } + + std::string msg; + for (size_t i = 0; i < titles.size(); ++i) { + // num — 0-based, но в сообщении показываем 1-based + msg += std::to_string(i + 1) + ". " + titles[i].name + "\n"; + } + return msg; +} \ No newline at end of file diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 88e026e..d19166a 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -20,6 +20,7 @@ void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) { processCallbackImpl(query); } +/* deprecated. will be deleted soon. HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) { // Здесь должен происходить запрос на сервер std::vector<BotStructs::Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}}; @@ -29,7 +30,7 @@ HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) { result.message = "1. Школа мертвяков\n2. KissXsis\n"; return result; -} +}*/ void BotHandlers::handleMessage(TgBot::Message::Ptr message) { //TODO: просмотр состояния пользователя @@ -60,62 +61,6 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { } } -void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { - const auto& current = ctx.history.back(); // текущий экран - const std::string& data = query->data; - - // Пагинация (в списках) - if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT) - && (current.state == UserState::VIEWING_MY_TITLES || current.state == UserState::VIEWING_REVIEW_LIST || - current.state == UserState::VIEWING_FOUND_TITLES)) { - - int64_t newPayload = current.payload; - if (data == BotConstants::Callback::LIST_PREV && newPayload > 0) { - reducePayload(newPayload, current.state); - } else if (data == BotConstants::Callback::LIST_NEXT) { - increasePayload(newPayload, current.state); - } else { - if (data == BotConstants::Callback::LIST_PREV) { - std::cout << "Error: navigation:prev callback for 1st page" << std::endl; - return; - } - // TODO: log - std::cout << "Error: navigation:prev unknown error" << std::endl; - } - - ctx.history.back().payload = newPayload; - - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); - return; - } - - // Обработка back по интерфейсу - if (data == BotConstants::Callback::NAV_BACK) { - if (!popState(ctx)) { - sendError(query, BotConstants::Text::SAD_ERROR); - return; - } - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); - return; - } - - // Переходы вперёд (push) - auto newStepOpt = computeNextStep(query, current); - if (!newStepOpt.has_value()) { - sendError(query, BotConstants::Text::SAD_ERROR); - return; - } - - ctx.history.push_back(*newStepOpt); - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); -} - void BotHandlers::pushState(UserContext& ctx, UserState newState, int64_t payload) { ctx.history.push_back({newState, payload}); } @@ -164,44 +109,6 @@ void BotHandlers::editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult res ); } -HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { - const auto& step = ctx.history.back(); - int64_t userId = query->from->id; - switch (step.state) { - case UserState::MAIN_MENU: - return showMainMenu(); - case UserState::VIEWING_MY_TITLES: - server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId - .then([this, query](pplx::task<std::vector<BotStructs::Title>> t) { - try { - auto titles = t.get(); - - std::string message = formatTitlesList(titles); - auto keyboard = KeyboardFactory::createMyTitles(titles); - - editMessage(query, {message, keyboard}); - - } catch (const std::exception& e) { - sendError(query, BotConstants::Text::SERVER_ERROR); - // Логирование ошибки (например, в cerr) - } - }); - - return {"meow", nullptr}; - /* - case UserState::VIEWING_TITLE_PAGE: - return returnTitlePage(step.payload); // payload = titleId - case UserState::VIEWING_REVIEW: - return returnReview(step.payload); // payload = reviewId - case UserState::AWAITING_REVIEW: - return HandlerResult{"Пришлите текст отзыва:", nullptr}; - // ... - */ - default: - return HandlerResult{BotConstants::Text::SAD_ERROR, nullptr}; - } -} - HandlerResult BotHandlers::showMainMenu() { auto keyboard = KeyboardFactory::createMainMenu(); @@ -221,42 +128,6 @@ void BotHandlers::sendError(TgBot::CallbackQuery::Ptr query, const std::string& editMessage(query, {errText, keyboard}); } -std::optional<NavigationStep> BotHandlers::computeNextStep( - const TgBot::CallbackQuery::Ptr& query, - const NavigationStep& current -) { - const std::string& data = query->data; - - switch (current.state) { - case UserState::MAIN_MENU: - if (data == BotConstants::Callback::MY_TITLES) { - return NavigationStep{UserState::VIEWING_MY_TITLES, 0}; - } - break; - /* - case UserState::VIEWING_MY_TITLES: - if (data.starts_with("title_")) { - int64_t titleId = parseId(data); - return NavigationStep{UserState::VIEWING_TITLE_PAGE, titleId}; - } - break; - - case UserState::VIEWING_TITLE_PAGE: - if (data == BotConstants::Callback::ACTION_ADD_REVIEW) { - return NavigationStep{UserState::AWAITING_REVIEW, current.payload}; - } - if (data.starts_with("review_")) { - int64_t reviewId = parseId(data); - return NavigationStep{UserState::VIEWING_REVIEW, reviewId}; - } - break; - */ - default: - break; - } - return std::nullopt; -} - void BotHandlers::createInitContext(int64_t chatId) { NavigationStep init = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD}; userContexts[chatId] = {chatId, {init}}; @@ -276,17 +147,4 @@ void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr}; editMessage(query, result); } -} - -std::string BotHandlers::formatTitlesList(const std::vector<BotStructs::Title>& titles) { - if (titles.empty()) { - return "У вас пока нет тайтлов."; - } - - std::string msg; - for (size_t i = 0; i < titles.size(); ++i) { - // num — 0-based, но в сообщении показываем 1-based - msg += std::to_string(i + 1) + ". " + titles[i].name + "\n"; - } - return msg; } \ No newline at end of file From 7e0222d6f1c4c1a18a546501f36ca59eacbb5180 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 6 Dec 2025 01:11:34 +0300 Subject: [PATCH 26/33] refactor(tgbot-front): change editMessage args --- modules/bot/front/include/handlers.hpp | 4 ++-- modules/bot/front/src/KeyboardFactory.cpp | 10 ++++----- modules/bot/front/src/handleNavigation.cpp | 24 +++++++++++++--------- modules/bot/front/src/handlers.cpp | 20 ++++++++++-------- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 873d74c..e1197fc 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -101,7 +101,7 @@ private: /// кнопки в интерфейсе /// @param query Callback запрос /// @param response Параметры ответа: клавиатура и текст - void editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult response); + void editMessage(int64_t chatId, int64_t messageId, HandlerResult response); /// @brief Отрисовка текущего экрана (соотв. контексту) /// @param ctx - текущий контекст @@ -121,7 +121,7 @@ private: /// @brief Посылает интерфейс обработки ошибки на callback запрос /// @param query запрос - void sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText); + void sendError(int64_t chatId, int64_t messageId, const std::string& errText); // Форматирование для отображения в сообщении std::string formatTitlesList(const std::vector<BotStructs::Title>& titles); diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index af6c1d8..b8bbd06 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -22,12 +22,12 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Bot int counter = 0; for(BotStructs::Title& title : titles) { - if(counter >= 6) { + if(counter >= BotConstants::DISP_TITLES_NUM) { break; } auto button = std::make_shared<TgBot::InlineKeyboardButton>(); - button->text = std::to_string(title.num) + " " + title.name; - button->callbackData = "title:" + std::to_string(title.num); + button->text = std::to_string(title.num + 1) + " " + title.name; + button->callbackData = BotConstants::Callback::CHOICE + std::to_string(title.num); row.push_back(button); counter++; if(counter % 2 == 0) { @@ -40,7 +40,7 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Bot if(counter % 2 == 1) { auto button = std::make_shared<TgBot::InlineKeyboardButton>(); button->text = BotConstants::Button::PREV; - if(titles[0].num == 1) { + if(titles[0].num == 0) { button->callbackData = BotConstants::Callback::NAV_BACK; } else { @@ -51,7 +51,7 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Bot else { auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); button_prev->text = BotConstants::Button::PREV; - if(titles[0].num == 1) { + if(titles[0].num == 0) { button_prev->callbackData = BotConstants::Callback::NAV_BACK; } else { diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp index 0bd02a3..c934885 100644 --- a/modules/bot/front/src/handleNavigation.cpp +++ b/modules/bot/front/src/handleNavigation.cpp @@ -6,6 +6,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { const auto& current = ctx.history.back(); // текущий экран const std::string& data = query->data; + int64_t chatId = query->message->chat->id; + int64_t messageId = query->message->messageId; // Пагинация (в списках) if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT) @@ -30,54 +32,56 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& auto result = renderCurrent(query, ctx); if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); + editMessage(chatId, messageId, result); return; } // Обработка back по интерфейсу if (data == BotConstants::Callback::NAV_BACK) { if (!popState(ctx)) { - sendError(query, BotConstants::Text::SAD_ERROR); + sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); return; } auto result = renderCurrent(query, ctx); if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); + editMessage(chatId, messageId, result); return; } - // Переходы вперёд (push) + // Переходы вперёд (pyush) auto newStepOpt = computeNextStep(query, current); if (!newStepOpt.has_value()) { - sendError(query, BotConstants::Text::SAD_ERROR); + sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); return; } ctx.history.push_back(*newStepOpt); auto result = renderCurrent(query, ctx); if(result.message == "meow") return; // TODO: убрать - editMessage(query, result); + editMessage(chatId, messageId, result); } HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { const auto& step = ctx.history.back(); - int64_t userId = query->from->id; + //int64_t userId = query->from->id; + int64_t chatId = query->message->chat->id; + int64_t messageId = query->message->messageId; switch (step.state) { case UserState::MAIN_MENU: return showMainMenu(); case UserState::VIEWING_MY_TITLES: server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId - .then([this, query](pplx::task<std::vector<BotStructs::Title>> t) { + .then([this, chatId, messageId](pplx::task<std::vector<BotStructs::Title>> t) { try { auto titles = t.get(); std::string message = formatTitlesList(titles); auto keyboard = KeyboardFactory::createMyTitles(titles); - editMessage(query, {message, keyboard}); + editMessage(chatId, messageId, {message, keyboard}); } catch (const std::exception& e) { - sendError(query, BotConstants::Text::SERVER_ERROR); + sendError(chatId, messageId, BotConstants::Text::SERVER_ERROR); // Логирование ошибки (например, в cerr) } }); diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index d19166a..64fc082 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -40,10 +40,12 @@ void BotHandlers::handleMessage(TgBot::Message::Ptr message) { void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { const std::string& data = query->data; int64_t userId = query->from->id; + int64_t chatId = query->message->chat->id; + int64_t messageId = query->message->messageId; auto it = userContexts.find(userId); if (it == userContexts.end()) { // TODO: log - sendError(query, BotConstants::Text::AUTH_ERROR); + sendError(chatId, messageId, BotConstants::Text::AUTH_ERROR); std::cout << "Error: Не нашел пользователя " << userId; return; } @@ -97,11 +99,11 @@ void BotHandlers::increasePayload(int64_t& payload, const UserState curState) { } } -void BotHandlers::editMessage(TgBot::CallbackQuery::Ptr query, HandlerResult response) { +void BotHandlers::editMessage(int64_t chatId, int64_t messageId, HandlerResult response) { botApi.editMessageText( response.message, - query->message->chat->id, - query->message->messageId, + chatId, + messageId, "", "", nullptr, @@ -115,7 +117,7 @@ HandlerResult BotHandlers::showMainMenu() { return HandlerResult{BotConstants::Text::MAIN_MENU, keyboard}; } -void BotHandlers::sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText) { +void BotHandlers::sendError(int64_t chatId, int64_t messageId, const std::string& errText) { //TODO: посылать сообщение с кнопкой возврата в главное меню TgBot::InlineKeyboardMarkup::Ptr keyboard; if (errText == BotConstants::Text::SAD_ERROR) { @@ -125,7 +127,7 @@ void BotHandlers::sendError(TgBot::CallbackQuery::Ptr query, const std::string& keyboard = nullptr; //KeyboardFactory::createError(BotConstants::Callback::ERROR_AUTH); } - editMessage(query, {errText, keyboard}); + editMessage(chatId, messageId, {errText, keyboard}); } void BotHandlers::createInitContext(int64_t chatId) { @@ -135,16 +137,18 @@ void BotHandlers::createInitContext(int64_t chatId) { void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { const std::string& data = query->data; + int64_t chatId = query->message->chat->id; + int64_t messageId = query->message->messageId; if(data == BotConstants::Callback::ERROR_NAVIGATION) { ctx.history.clear(); ctx.history.push_back({UserState::MAIN_MENU, 0}); auto result = showMainMenu(); - editMessage(query, result); + editMessage(chatId, messageId, result); } else if(data == BotConstants::Callback::ERROR_AUTH) { // TODO: продумать логику HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr}; - editMessage(query, result); + editMessage(chatId, messageId, result); } } \ No newline at end of file From a7b47c564a593a77b568438fc751fc5f6a0e3b93 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 6 Dec 2025 01:59:28 +0300 Subject: [PATCH 27/33] refactor(tgbot-front): small cosmetics --- modules/bot/front/include/handlers.hpp | 3 +-- modules/bot/front/src/handleNavigation.cpp | 23 +++++++++------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index e1197fc..417025f 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -105,8 +105,7 @@ private: /// @brief Отрисовка текущего экрана (соотв. контексту) /// @param ctx - текущий контекст - /// @return HandlerResult для нового состояния сообщения - HandlerResult renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx); + void renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx); /// @brief Логика переходов между контекстами (навигация на следующий шаг) /// @param query - запрос diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp index c934885..b0515ae 100644 --- a/modules/bot/front/src/handleNavigation.cpp +++ b/modules/bot/front/src/handleNavigation.cpp @@ -30,9 +30,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx.history.back().payload = newPayload; - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(chatId, messageId, result); + renderCurrent(query, ctx); return; } @@ -42,13 +40,11 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); return; } - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(chatId, messageId, result); + renderCurrent(query, ctx); return; } - // Переходы вперёд (pyush) + // Переходы вперёд (push) auto newStepOpt = computeNextStep(query, current); if (!newStepOpt.has_value()) { sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); @@ -56,19 +52,18 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& } ctx.history.push_back(*newStepOpt); - auto result = renderCurrent(query, ctx); - if(result.message == "meow") return; // TODO: убрать - editMessage(chatId, messageId, result); + renderCurrent(query, ctx); } -HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { +void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { const auto& step = ctx.history.back(); //int64_t userId = query->from->id; int64_t chatId = query->message->chat->id; int64_t messageId = query->message->messageId; switch (step.state) { case UserState::MAIN_MENU: - return showMainMenu(); + editMessage(chatId, messageId, showMainMenu()); + return; case UserState::VIEWING_MY_TITLES: server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId .then([this, chatId, messageId](pplx::task<std::vector<BotStructs::Title>> t) { @@ -86,7 +81,7 @@ HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const } }); - return {"meow", nullptr}; + return; /* case UserState::VIEWING_TITLE_PAGE: return returnTitlePage(step.payload); // payload = titleId @@ -97,7 +92,7 @@ HandlerResult BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const // ... */ default: - return HandlerResult{BotConstants::Text::SAD_ERROR, nullptr}; + return editMessage(chatId, messageId, HandlerResult{BotConstants::Text::SAD_ERROR, nullptr}); } } From b1c035ae350442814f788fae22dae5cbd8d4da9f Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 6 Dec 2025 02:44:24 +0300 Subject: [PATCH 28/33] feat(tgbot-front): start creating thread-safe user context --- modules/bot/front/include/BotUserContext.hpp | 55 ++++++++++++++++++++ modules/bot/front/src/BotUserContext.cpp | 43 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 modules/bot/front/include/BotUserContext.hpp create mode 100644 modules/bot/front/src/BotUserContext.cpp diff --git a/modules/bot/front/include/BotUserContext.hpp b/modules/bot/front/include/BotUserContext.hpp new file mode 100644 index 0000000..bf69058 --- /dev/null +++ b/modules/bot/front/include/BotUserContext.hpp @@ -0,0 +1,55 @@ +#pragma once +#include <unordered_map> +#include <vector> +#include <mutex> +#include <optional> + +enum class UserState { + MAIN_MENU, // Главное меню + VIEWING_MY_TITLES, // Список моих тайтлов + AWAITING_TITLE_NAME, // Жду название тайтла для поиска + VIEWING_FOUND_TITLES, // Смотрю найденные тайтлы + VIEWING_TITLE_PAGE, // Смотрю страничку тайтла + AWAITING_REVIEW, // Жду ревью на тайтл + VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл + VIEWING_REVIEW, // Смотрю (конкретное) ревью на тайтл + VIEWING_DESCRIPTION, // Смотрю описание тайтла + ERROR, // Ошибка состояния +}; + +struct NavigationStep { + UserState state; + int64_t payload; // ID тайтла, ревью и т.д. +}; + + +struct UserContext { + int64_t userId; + std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний +}; + +class BotUserContext { +private: + mutable std::mutex mtx; + std::unordered_map<int64_t, UserContext> userContexts; + +public: + // Получить копию контекста пользователя (или std::nullopt, если не найден) + std::optional<UserContext> getContext(int64_t userId) const; + + // Установить/обновить контекст пользователя + void setContext(int64_t userId, const UserContext& context); + + // Добавить шаг навигации к существующему контексту пользователя + // Если пользователя нет — создаётся новый контекст + void pushNavigationStep(int64_t userId, const NavigationStep& step); + + // Заменить текущую историю (полезно, например, при сбросе состояния) + void setNavigationHistory(int64_t userId, const std::vector<NavigationStep>& history); + + // Получить текущий шаг (последний в истории) или std::nullopt, если нет истории + std::optional<NavigationStep> getCurrentStep(int64_t userId) const; + + // Удалить контекст пользователя (например, при логауте) + void removeContext(int64_t userId); +}; \ No newline at end of file diff --git a/modules/bot/front/src/BotUserContext.cpp b/modules/bot/front/src/BotUserContext.cpp new file mode 100644 index 0000000..04a423c --- /dev/null +++ b/modules/bot/front/src/BotUserContext.cpp @@ -0,0 +1,43 @@ +#include "BotUserContext.hpp" + +std::optional<UserContext> BotUserContext::getContext(int64_t userId) const { + std::lock_guard<std::mutex> lock(mtx); + auto it = userContexts.find(userId); + if (it != userContexts.end()) { + return it->second; + } + return std::nullopt; +} + +void BotUserContext::setContext(int64_t userId, const UserContext& context) { + std::lock_guard<std::mutex> lock(mtx); + userContexts[userId] = context; +} + +void BotUserContext::pushNavigationStep(int64_t userId, const NavigationStep& step) { + std::lock_guard<std::mutex> lock(mtx); + auto& ctx = userContexts[userId]; + ctx.userId = userId; + ctx.history.push_back(step); +} + +void BotUserContext::setNavigationHistory(int64_t userId, const std::vector<NavigationStep>& history) { + std::lock_guard<std::mutex> lock(mtx); + auto& ctx = userContexts[userId]; + ctx.userId = userId; + ctx.history = history; +} + +std::optional<NavigationStep> BotUserContext::getCurrentStep(int64_t userId) const { + std::lock_guard<std::mutex> lock(mtx); + auto it = userContexts.find(userId); + if (it != userContexts.end() && !it->second.history.empty()) { + return it->second.history.back(); + } + return std::nullopt; +} + +void BotUserContext::removeContext(int64_t userId) { + std::lock_guard<std::mutex> lock(mtx); + userContexts.erase(userId); +} \ No newline at end of file From a6848bb4d7589075060a2afd6f6f5b214463aaaf Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 6 Dec 2025 05:02:34 +0300 Subject: [PATCH 29/33] refactor(tgbot-front): replaced the context usage with a new thread-safe one --- modules/bot/front/include/BotUserContext.hpp | 8 ++++ modules/bot/front/include/handlers.hpp | 49 +++----------------- modules/bot/front/src/BotUserContext.cpp | 15 ++++++ modules/bot/front/src/front.cpp | 2 +- modules/bot/front/src/handleNavigation.cpp | 37 ++++++++++----- 5 files changed, 55 insertions(+), 56 deletions(-) diff --git a/modules/bot/front/include/BotUserContext.hpp b/modules/bot/front/include/BotUserContext.hpp index bf69058..a3e2609 100644 --- a/modules/bot/front/include/BotUserContext.hpp +++ b/modules/bot/front/include/BotUserContext.hpp @@ -3,6 +3,7 @@ #include <vector> #include <mutex> #include <optional> +#include "constants.hpp" enum class UserState { MAIN_MENU, // Главное меню @@ -50,6 +51,13 @@ public: // Получить текущий шаг (последний в истории) или std::nullopt, если нет истории std::optional<NavigationStep> getCurrentStep(int64_t userId) const; + // pop последнего состояния. true в случае удачи + bool popStep(int64_t userId); + // Удалить контекст пользователя (например, при логауте) void removeContext(int64_t userId); + + /// @brief Создает контекст начального меню для пользователя + /// @param userId + void createInitContext(int64_t userId); }; \ No newline at end of file diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 417025f..327c17b 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -4,6 +4,7 @@ #include <structs.hpp> #include <unordered_map> #include "BotToServer.hpp" +#include "BotUserContext.hpp" /// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения struct HandlerResult { @@ -11,30 +12,6 @@ struct HandlerResult { TgBot::InlineKeyboardMarkup::Ptr keyboard; }; -enum class UserState { - MAIN_MENU, // Главное меню - VIEWING_MY_TITLES, // Список моих тайтлов - AWAITING_TITLE_NAME, // Жду название тайтла для поиска - VIEWING_FOUND_TITLES, // Смотрю найденные тайтлы - VIEWING_TITLE_PAGE, // Смотрю страничку тайтла - AWAITING_REVIEW, // Жду ревью на тайтл - VIEWING_REVIEW_LIST, // Смотрю список ревью на тайтл - VIEWING_REVIEW, // Смотрю (конкретное) ревью на тайтл - VIEWING_DESCRIPTION, // Смотрю описание тайтла - ERROR, // Ошибка состояния -}; - -struct NavigationStep { - UserState state; - int64_t payload; // ID тайтла, ревью и т.д. -}; - - -struct UserContext { - int64_t userId; - std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний -}; - class BotHandlers { public: BotHandlers(TgBot::Api api) : botApi(api) {;} @@ -51,19 +28,16 @@ public: /// в боте. /// @param message обрабатываемое сообщение void handleMessage(TgBot::Message::Ptr message); - - /// @brief Создает контекст начального меню для пользователя - /// @param chatId id чата пользователя - void createInitContext(int64_t chatId); + void initUser(int64_t userId); private: TgBot::Api botApi; - std::unordered_map<int64_t, UserContext> userContexts; + BotUserContext contextManager; BotToServer server_; - void handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx); + void handleNavigation(TgBot::CallbackQuery::Ptr query); - void handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx); + void handleError(TgBot::CallbackQuery::Ptr query); void processCallbackImpl(TgBot::CallbackQuery::Ptr query); @@ -73,17 +47,6 @@ private: /// @return HandlerResult /// static HandlerResult returnMyTitles(int64_t userId, int64_t payload); - /// @brief Вход в новое состояние - /// @param ctx текущий контекст - /// @param newState новое состояние, добавляемое в стек - /// @param payload полезная нагрузка этого состояния - void pushState(UserContext& ctx, UserState newState, int64_t payload); - - /// @brief Возврат в предыдущее состояние - /// @param ctx Текущий контекст - /// @return true в случае успеха - bool popState(UserContext& ctx); - /// @brief Уменьшает значение нагрузки с учетом текущего состояния /// @param payload Изменяемое значение нагрузки /// @param curState Текущее состояние @@ -105,7 +68,7 @@ private: /// @brief Отрисовка текущего экрана (соотв. контексту) /// @param ctx - текущий контекст - void renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx); + void renderCurrent(TgBot::CallbackQuery::Ptr query); /// @brief Логика переходов между контекстами (навигация на следующий шаг) /// @param query - запрос diff --git a/modules/bot/front/src/BotUserContext.cpp b/modules/bot/front/src/BotUserContext.cpp index 04a423c..2b2eb41 100644 --- a/modules/bot/front/src/BotUserContext.cpp +++ b/modules/bot/front/src/BotUserContext.cpp @@ -40,4 +40,19 @@ std::optional<NavigationStep> BotUserContext::getCurrentStep(int64_t userId) con void BotUserContext::removeContext(int64_t userId) { std::lock_guard<std::mutex> lock(mtx); userContexts.erase(userId); +} + +bool BotUserContext::popStep(int64_t userId) { + std::lock_guard<std::mutex> lock(mtx); + auto it = userContexts.find(userId); + if (it != userContexts.end() && (it->second.history.size() > 1)) { + it->second.history.pop_back(); + return true; + } + return false; +} + +void BotUserContext::createInitContext(int64_t userId) { + NavigationStep initStep = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD}; + setContext(userId, {userId, {initStep}}); } \ No newline at end of file diff --git a/modules/bot/front/src/front.cpp b/modules/bot/front/src/front.cpp index 4f463e1..b9abd73 100644 --- a/modules/bot/front/src/front.cpp +++ b/modules/bot/front/src/front.cpp @@ -13,7 +13,7 @@ void AnimeBot::setupHandlers() { bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { sendMainMenu(message->chat->id); //TODO: производить инициализацию контекста только после авторизации - handler.createInitContext(message->chat->id); + handler.initUser(message->from->id); }); bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) { diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp index b0515ae..b8af2ef 100644 --- a/modules/bot/front/src/handleNavigation.cpp +++ b/modules/bot/front/src/handleNavigation.cpp @@ -3,13 +3,15 @@ #include "structs.hpp" #include "constants.hpp" -void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { - const auto& current = ctx.history.back(); // текущий экран +void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) { + //const auto& current = ctx.history.back(); // текущий экран const std::string& data = query->data; + int64_t userId = query->from->id; int64_t chatId = query->message->chat->id; int64_t messageId = query->message->messageId; // Пагинация (в списках) + /* Временно отключаем, все равно не функционирует :) if ((data == BotConstants::Callback::LIST_PREV || data == BotConstants::Callback::LIST_NEXT) && (current.state == UserState::VIEWING_MY_TITLES || current.state == UserState::VIEWING_REVIEW_LIST || current.state == UserState::VIEWING_FOUND_TITLES)) { @@ -32,35 +34,46 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& renderCurrent(query, ctx); return; - } + }*/ // Обработка back по интерфейсу if (data == BotConstants::Callback::NAV_BACK) { - if (!popState(ctx)) { + if (!contextManager.popStep(userId)) { sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); return; } - renderCurrent(query, ctx); + renderCurrent(query); return; } // Переходы вперёд (push) - auto newStepOpt = computeNextStep(query, current); + std::optional<NavigationStep> currentStep = contextManager.getCurrentStep(userId); + if(!currentStep.has_value()) { + sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); + return; + } + auto newStepOpt = computeNextStep(query, currentStep.value()); if (!newStepOpt.has_value()) { sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); return; } - ctx.history.push_back(*newStepOpt); - renderCurrent(query, ctx); + contextManager.pushNavigationStep(userId, newStepOpt.value()); + renderCurrent(query); } -void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx) { - const auto& step = ctx.history.back(); - //int64_t userId = query->from->id; +void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query) { + int64_t userId = query->from->id; int64_t chatId = query->message->chat->id; int64_t messageId = query->message->messageId; - switch (step.state) { + + auto step = contextManager.getCurrentStep(userId); + if(!step.has_value()) {; + sendError(chatId, messageId, BotConstants::Text::SAD_ERROR); + return; + } + + switch (step.value().state) { case UserState::MAIN_MENU: editMessage(chatId, messageId, showMainMenu()); return; From dc4c430231bc3ec674eea4f8087029a667e65873 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 6 Dec 2025 05:05:10 +0300 Subject: [PATCH 30/33] refactor(tgbot-front): replaced the context usage with a new thread-safe one --- modules/bot/front/src/handlers.cpp | 37 +++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 64fc082..0555cc3 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -42,37 +42,26 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { int64_t userId = query->from->id; int64_t chatId = query->message->chat->id; int64_t messageId = query->message->messageId; - auto it = userContexts.find(userId); - if (it == userContexts.end()) { + + std::optional<UserContext> ctx = contextManager.getContext(userId); + if (!ctx.has_value()) { // TODO: log sendError(chatId, messageId, BotConstants::Text::AUTH_ERROR); std::cout << "Error: Не нашел пользователя " << userId; return; } - UserContext& ctx = it->second; - if (data.starts_with(BotConstants::Callback::NAVIGATION)) { - handleNavigation(query, ctx); + handleNavigation(query); } else if (data.starts_with(BotConstants::Callback::ERROR)) { - handleError(query, ctx); + handleError(query); } else { botApi.sendMessage(query->message->chat->id, BotConstants::Text::SAD_ERROR, nullptr, nullptr); } } -void BotHandlers::pushState(UserContext& ctx, UserState newState, int64_t payload) { - ctx.history.push_back({newState, payload}); -} - -bool BotHandlers::popState(UserContext& ctx) { - if (ctx.history.size() <= 1) return false; // нельзя выйти из MAIN_MENU - ctx.history.pop_back(); - return true; -} - void BotHandlers::reducePayload(int64_t& payload, const UserState curState) { if (curState == UserState::VIEWING_MY_TITLES || curState == UserState::VIEWING_FOUND_TITLES) { @@ -130,19 +119,15 @@ void BotHandlers::sendError(int64_t chatId, int64_t messageId, const std::string editMessage(chatId, messageId, {errText, keyboard}); } -void BotHandlers::createInitContext(int64_t chatId) { - NavigationStep init = {UserState::MAIN_MENU, BotConstants::NULL_PAYLOAD}; - userContexts[chatId] = {chatId, {init}}; -} - -void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) { +void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query) { const std::string& data = query->data; + int64_t userId = query->from->id; int64_t chatId = query->message->chat->id; int64_t messageId = query->message->messageId; if(data == BotConstants::Callback::ERROR_NAVIGATION) { - ctx.history.clear(); - ctx.history.push_back({UserState::MAIN_MENU, 0}); + contextManager.removeContext(userId); + contextManager.createInitContext(userId); auto result = showMainMenu(); editMessage(chatId, messageId, result); } @@ -151,4 +136,8 @@ void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx) HandlerResult result = {BotConstants::Text::AUTH_ERROR, nullptr}; editMessage(chatId, messageId, result); } +} + +void BotHandlers::initUser(int64_t userId) { + contextManager.createInitContext(userId); } \ No newline at end of file From d6194ec8be5bc564b09e74695715892e66022313 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 19 Dec 2025 17:40:21 +0300 Subject: [PATCH 31/33] fix: 1 userTitle case error --- modules/bot/front/src/KeyboardFactory.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/bot/front/src/KeyboardFactory.cpp b/modules/bot/front/src/KeyboardFactory.cpp index b8bbd06..d7a9d47 100644 --- a/modules/bot/front/src/KeyboardFactory.cpp +++ b/modules/bot/front/src/KeyboardFactory.cpp @@ -35,6 +35,10 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Bot row.clear(); } } + if (!row.empty()) { + layout.push_back(row); + row.clear(); + } // TODO: Додумать логику, когда пришло 6 записей в конце if(counter % 2 == 1) { @@ -46,7 +50,7 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Bot else { button->callbackData = BotConstants::Callback::LIST_PREV + ':' + std::to_string(titles[0].num); } - layout[counter / 2].push_back(button); + layout.back().push_back(button); } else { auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); From f045eb22b27f058d9de01f134d087ca28ddef78c Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Fri, 19 Dec 2025 17:48:16 +0300 Subject: [PATCH 32/33] feat(tgbot-back): Add functions for processing user authentication --- modules/bot/CMakeLists.txt | 4 ++ .../bot/back/include/AuthImpersonation.hpp | 35 ++++++++++ modules/bot/back/include/BotToServer.hpp | 4 ++ modules/bot/back/src/AuthImpersonation.cpp | 34 ++++++++++ modules/bot/back/src/BotToServer.cpp | 64 +++++++++++++------ modules/bot/front/src/handleNavigation.cpp | 2 +- 6 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 modules/bot/back/include/AuthImpersonation.hpp create mode 100644 modules/bot/back/src/AuthImpersonation.cpp diff --git a/modules/bot/CMakeLists.txt b/modules/bot/CMakeLists.txt index 7a8948d..b6254c1 100644 --- a/modules/bot/CMakeLists.txt +++ b/modules/bot/CMakeLists.txt @@ -11,6 +11,9 @@ list(APPEND SOURCES ${SRC_BACK}) file(GLOB_RECURSE SRC_API "generated-client/src/*.cpp") list(APPEND SOURCES ${SRC_API}) +file(GLOB_RECURSE SRC_AUTH "generated-client-auth/src/*.cpp") +list(APPEND SOURCES ${SRC_AUTH}) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") @@ -26,6 +29,7 @@ include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DI include_directories(front/include/) include_directories(back/include) include_directories(generated-client/include) +include_directories(generated-client-auth/include) if (CURL_FOUND) include_directories(${CURL_INCLUDE_DIRS}) add_definitions(-DHAVE_CURL) diff --git a/modules/bot/back/include/AuthImpersonation.hpp b/modules/bot/back/include/AuthImpersonation.hpp new file mode 100644 index 0000000..67d25fe --- /dev/null +++ b/modules/bot/back/include/AuthImpersonation.hpp @@ -0,0 +1,35 @@ +// AuthImpersonationClient.hpp + +#pragma once + +#include <memory> +#include <string> +#include <stdexcept> +#include <cstdlib> +#include <map> +#include <cpprest/asyncrt_utils.h> +#include "AuthClient/ApiClient.h" +#include "AuthClient/ApiConfiguration.h" +#include "AuthClient/api/AuthApi.h" +#include "AuthClient/model/GetImpersonationToken_request.h" +#include "AuthClient/model/GetImpersonationToken_200_response.h" + + +namespace nyanimed { + +class AuthImpersonationClient { +public: + AuthImpersonationClient(); + + // Потокобезопасный вызов — не модифицирует состояние + pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> + getImpersonationToken(int64_t userId) const; + +private: + std::string m_baseUrl; + std::string m_authToken; + std::shared_ptr<nyanimed::meow::auth::api::ApiClient> m_apiClient; + std::shared_ptr<nyanimed::meow::auth::api::AuthApi> m_authApi; +}; + +} // namespace nyanimed \ No newline at end of file diff --git a/modules/bot/back/include/BotToServer.hpp b/modules/bot/back/include/BotToServer.hpp index 7f10fb6..d4b132b 100644 --- a/modules/bot/back/include/BotToServer.hpp +++ b/modules/bot/back/include/BotToServer.hpp @@ -15,6 +15,8 @@ #include <cpprest/asyncrt_utils.h> #include <boost/optional.hpp> +#include "AuthImpersonation.hpp" + using namespace org::openapitools::client::api; class BotToServer { @@ -28,4 +30,6 @@ private: std::shared_ptr<org::openapitools::client::api::ApiConfiguration> apiconfiguration; std::shared_ptr<org::openapitools::client::api::ApiClient> apiclient; std::shared_ptr<org::openapitools::client::api::DefaultApi> api; + + nyanimed::AuthImpersonationClient authClient; }; \ No newline at end of file diff --git a/modules/bot/back/src/AuthImpersonation.cpp b/modules/bot/back/src/AuthImpersonation.cpp new file mode 100644 index 0000000..f111922 --- /dev/null +++ b/modules/bot/back/src/AuthImpersonation.cpp @@ -0,0 +1,34 @@ +#include "AuthImpersonation.hpp" + +nyanimed::AuthImpersonationClient::AuthImpersonationClient() { + const char* baseUrlEnv = std::getenv("NYANIMEDBAUTHURL"); + const char* tokenEnv = std::getenv("NYANIMEDBAUTHTOKEN"); + + if (!baseUrlEnv || std::string(baseUrlEnv).empty()) { + throw std::runtime_error("Missing required environment variable: NYANIMEDBAUTHURL"); + } + if (!tokenEnv || std::string(tokenEnv).empty()) { + throw std::runtime_error("Missing required environment variable: NYANIMEDBAUTHTOKEN"); + } + + m_baseUrl = baseUrlEnv; + m_authToken = tokenEnv; + + auto config = std::make_shared<nyanimed::meow::auth::api::ApiConfiguration>(); + config->setBaseUrl(utility::conversions::to_string_t(m_baseUrl)); + + m_apiClient = std::make_shared<nyanimed::meow::auth::api::ApiClient>(config); + m_authApi = std::make_shared<nyanimed::meow::auth::api::AuthApi>(m_apiClient); +} + +pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> +nyanimed::AuthImpersonationClient::getImpersonationToken(int64_t userId) const { + auto request = std::make_shared<nyanimed::meow::auth::model::GetImpersonationToken_request>(); + request->setUserId(userId); + //request->setExternalId(externalId); + + std::map<utility::string_t, utility::string_t> headers; + headers[U("Authorization")] = U("Bearer ") + utility::conversions::to_string_t(m_authToken); + + return m_authApi->getImpersonationToken(request, headers); +} \ No newline at end of file diff --git a/modules/bot/back/src/BotToServer.cpp b/modules/bot/back/src/BotToServer.cpp index 8897ac4..e9d33ff 100644 --- a/modules/bot/back/src/BotToServer.cpp +++ b/modules/bot/back/src/BotToServer.cpp @@ -45,28 +45,52 @@ static BotStructs::Title mapUserTitleToBotTitle( } pplx::task<std::vector<BotStructs::Title>> BotToServer::fetchUserTitlesAsync(const std::string& userId) { - utility::string_t userIdW = utility::conversions::to_string_t(userId); - int32_t limit = static_cast<int32_t>(BotConstants::DISP_TITLES_NUM); + // Шаг 1: Получаем impersonation-токен + auto impersonationTask = authClient.getImpersonationToken(std::stoi(userId)); - auto responseTask = api->getUserTitles( - userIdW, - boost::none, // cursor - boost::none, // sort - boost::none, // sortForward - boost::none, // word - boost::none, // status - boost::none, // watchStatus - boost::none, // rating - boost::none, // myRate - boost::none, // releaseYear - boost::none, // releaseSeason - limit, - boost::none // fields - ); - - return responseTask.then([=](pplx::task<std::shared_ptr<org::openapitools::client::model::GetUserTitles_200_response>> task) { + // Шаг 2: После получения токена — делаем запрос getUserTitles с этим токеном + return impersonationTask.then([=](pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> tokenTask) { try { - auto response = task.get(); + auto tokenResponse = tokenTask.get(); + if (!tokenResponse) { + throw std::runtime_error("Null response from getImpersonationToken"); + } + + utility::string_t accessToken = utility::conversions::to_string_t(tokenResponse->getAccessToken()); + + // Формируем заголовки с токеном + std::map<utility::string_t, utility::string_t> customHeaders; + customHeaders[U("Cookie")] = U("access_token=") + accessToken; + + // Подготавливаем параметры запроса + utility::string_t userIdW = utility::conversions::to_string_t(userId); + int32_t limit = static_cast<int32_t>(BotConstants::DISP_TITLES_NUM); + + // Шаг 3: Выполняем getUserTitles с кастомными заголовками + return api->getUserTitles( + userIdW, + boost::none, // cursor + boost::none, // sort + boost::none, // sortForward + boost::none, // word + boost::none, // status + boost::none, // watchStatus + boost::none, // rating + boost::none, // myRate + boost::none, // releaseYear + boost::none, // releaseSeason + limit, + boost::none, // fields + customHeaders + ); + + } catch (const std::exception& e) { + std::cerr << "Error obtaining impersonation token: " << e.what() << std::endl; + throw; // Пробрасываем, чтобы цепочка task.then завершилась с ошибкой + } + }).then([=](pplx::task<std::shared_ptr<org::openapitools::client::model::GetUserTitles_200_response>> responseTask) { + try { + auto response = responseTask.get(); if (!response) { throw std::runtime_error("Null response from getUserTitles"); } diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp index b8af2ef..ded36b1 100644 --- a/modules/bot/front/src/handleNavigation.cpp +++ b/modules/bot/front/src/handleNavigation.cpp @@ -78,7 +78,7 @@ void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query) { editMessage(chatId, messageId, showMainMenu()); return; case UserState::VIEWING_MY_TITLES: - server_.fetchUserTitlesAsync(std::to_string(2)) // ALARM: тестовое значение вместо userId + server_.fetchUserTitlesAsync(std::to_string(22)) // ALARM: тестовое значение вместо userId .then([this, chatId, messageId](pplx::task<std::vector<BotStructs::Title>> t) { try { auto titles = t.get(); From da40d7df499a02454d3fa7777dc683703c747ea3 Mon Sep 17 00:00:00 2001 From: Kirill <mymail@mail.com> Date: Sat, 20 Dec 2025 01:19:56 +0300 Subject: [PATCH 33/33] feat(tgbot): add title menu --- modules/bot/back/include/BotToServer.hpp | 4 + modules/bot/back/src/BotToServer.cpp | 99 +++++++++++++++++++ modules/bot/front/include/KeyboardFactory.hpp | 3 + modules/bot/front/include/handlers.hpp | 22 +++++ modules/bot/front/include/structs.hpp | 1 + modules/bot/front/src/handleNavigation.cpp | 70 ++++++++++++- modules/bot/front/src/handlers.cpp | 56 +++++++++-- 7 files changed, 244 insertions(+), 11 deletions(-) diff --git a/modules/bot/back/include/BotToServer.hpp b/modules/bot/back/include/BotToServer.hpp index d4b132b..36e6943 100644 --- a/modules/bot/back/include/BotToServer.hpp +++ b/modules/bot/back/include/BotToServer.hpp @@ -25,6 +25,7 @@ public: // Асинхронный метод: получить список тайтлов пользователя pplx::task<std::vector<BotStructs::Title>> fetchUserTitlesAsync(const std::string& userId); + pplx::task<BotStructs::Title> fetchTitleAsync(const std::string& userId, int64_t titleId); private: std::shared_ptr<org::openapitools::client::api::ApiConfiguration> apiconfiguration; @@ -32,4 +33,7 @@ private: std::shared_ptr<org::openapitools::client::api::DefaultApi> api; nyanimed::AuthImpersonationClient authClient; + + static BotStructs::Title mapTitleToBotTitle( + const std::shared_ptr<org::openapitools::client::model::Title>& titleModel); }; \ No newline at end of file diff --git a/modules/bot/back/src/BotToServer.cpp b/modules/bot/back/src/BotToServer.cpp index e9d33ff..d403a01 100644 --- a/modules/bot/back/src/BotToServer.cpp +++ b/modules/bot/back/src/BotToServer.cpp @@ -114,4 +114,103 @@ pplx::task<std::vector<BotStructs::Title>> BotToServer::fetchUserTitlesAsync(con throw; } }); +} + +pplx::task<BotStructs::Title> BotToServer::fetchTitleAsync(const std::string& userId, int64_t titleId) { + auto impersonationTask = authClient.getImpersonationToken(std::stoi(userId)); + + // Шаг 2: После получения токена — делаем запрос getTitle с этим токеном + return impersonationTask.then([=](pplx::task<std::shared_ptr<nyanimed::meow::auth::model::GetImpersonationToken_200_response>> tokenTask) { + try { + auto tokenResponse = tokenTask.get(); + if (!tokenResponse) { + throw std::runtime_error("Null response from getImpersonationToken"); + } + + utility::string_t accessToken = utility::conversions::to_string_t(tokenResponse->getAccessToken()); + + // Формируем заголовки с токеном + std::map<utility::string_t, utility::string_t> customHeaders; + customHeaders[U("Cookie")] = U("access_token=") + accessToken; + + // Шаг 3: Выполняем запрос getTitle + return api->getTitle( + titleId, + boost::none, // fields — оставляем по умолчанию (все поля) + customHeaders + ); + + } catch (const std::exception& e) { + std::cerr << "Error obtaining impersonation token in fetchTitleAsync: " << e.what() << std::endl; + throw; + } + }).then([](pplx::task<std::shared_ptr<org::openapitools::client::model::Title>> responseTask) { + try { + auto response = responseTask.get(); + if (!response) { + throw std::runtime_error("Null response from getTitle"); + } + + // Преобразуем модель OpenAPI в внутреннюю структуру бота + BotStructs::Title botTitle = mapTitleToBotTitle(response); + return botTitle; + + } catch (const web::http::http_exception& e) { + std::cerr << "HTTP error in fetchTitleAsync: " << e.what() << std::endl; + throw; + } catch (const std::exception& e) { + std::cerr << "Error in fetchTitleAsync: " << e.what() << std::endl; + throw; + } + }); +} + +BotStructs::Title BotToServer::mapTitleToBotTitle( + const std::shared_ptr<org::openapitools::client::model::Title>& titleModel) +{ + if (!titleModel) { + throw std::invalid_argument("titleModel is null"); + } + + BotStructs::Title botTitle; + botTitle.id = titleModel->getId(); + botTitle.num = 0; + botTitle.description = ""; // Описание недоступно в текущей модели + + // Извлекаем название + std::string titleName; + const auto& titleNames = titleModel->getTitleNames(); + + // Попробуем ru → en → первый попавшийся + std::vector<utility::string_t> preferredLangs = { U("ru"), U("en") }; + bool found = false; + for (const auto& lang : preferredLangs) { + auto it = titleNames.find(lang); + if (it != titleNames.end() && !it->second.empty()) { + titleName = utility::conversions::to_utf8string(it->second[0]); + found = true; + break; + } + } + + if (!found && !titleNames.empty()) { + // Берём первый язык и первое название + const auto& firstLang = *titleNames.begin(); + if (!firstLang.second.empty()) { + titleName = utility::conversions::to_utf8string(firstLang.second[0]); + } + } + + botTitle.name = titleName; + + // --- Изображение --- + botTitle.imageUrl = ""; + if (titleModel->posterIsSet()) { + auto poster = titleModel->getPoster(); + if (poster && poster->imagePathIsSet()) { + botTitle.imageUrl = utility::conversions::to_utf8string(poster->getImagePath()); + } + } + + return botTitle; } \ No newline at end of file diff --git a/modules/bot/front/include/KeyboardFactory.hpp b/modules/bot/front/include/KeyboardFactory.hpp index f51b5a1..6a2dd52 100644 --- a/modules/bot/front/include/KeyboardFactory.hpp +++ b/modules/bot/front/include/KeyboardFactory.hpp @@ -11,4 +11,7 @@ public: /// Create keyboard for sendError static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback); + + /// Create keyboard for Title page + static TgBot::InlineKeyboardMarkup::Ptr createTitleMenu(int64_t title_id); }; diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 327c17b..a86f484 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -3,6 +3,9 @@ #include <string> #include <structs.hpp> #include <unordered_map> +#include <string> +#include <stdexcept> +#include <cctype> #include "BotToServer.hpp" #include "BotUserContext.hpp" @@ -66,6 +69,19 @@ private: /// @param response Параметры ответа: клавиатура и текст void editMessage(int64_t chatId, int64_t messageId, HandlerResult response); + /// @brief Редактирует текущее сообщение в диалоге с пользователем + /// @details Меняет текст сообщения и клавиатуру на те, что передаются + /// в аргументе response, а также прикрепляет картинку + /// @param chatId id чата + /// @param messageId id меняемого сообщения + /// @param imageUrl ссылка на вставляемую картинку (должна быть доступна в публичной сети) + /// @param response Параметры ответа: клавиатура и текст + void editMessageWithPhoto( + int64_t chatId, + int64_t messageId, + const std::string& imageUrl, + HandlerResult response); + /// @brief Отрисовка текущего экрана (соотв. контексту) /// @param ctx - текущий контекст void renderCurrent(TgBot::CallbackQuery::Ptr query); @@ -87,4 +103,10 @@ private: // Форматирование для отображения в сообщении std::string formatTitlesList(const std::vector<BotStructs::Title>& titles); + + // Форматирование сообщения на страничке с тайтлом + std::string formatTitle(const BotStructs::Title& title); + + // Парсинг id тайтла из callback'а + int64_t parseId(const std::string& data); }; diff --git a/modules/bot/front/include/structs.hpp b/modules/bot/front/include/structs.hpp index d702163..0db7925 100644 --- a/modules/bot/front/include/structs.hpp +++ b/modules/bot/front/include/structs.hpp @@ -6,5 +6,6 @@ struct Title { std::string name; std::string description; int64_t num; + std::string imageUrl; }; } diff --git a/modules/bot/front/src/handleNavigation.cpp b/modules/bot/front/src/handleNavigation.cpp index ded36b1..41ab3ec 100644 --- a/modules/bot/front/src/handleNavigation.cpp +++ b/modules/bot/front/src/handleNavigation.cpp @@ -95,9 +95,28 @@ void BotHandlers::renderCurrent(TgBot::CallbackQuery::Ptr query) { }); return; - /* + case UserState::VIEWING_TITLE_PAGE: - return returnTitlePage(step.payload); // payload = titleId + server_.fetchTitleAsync(std::to_string(22), step.value().payload) + .then([this, chatId, messageId](pplx::task<BotStructs::Title> t) { + try { + auto title = t.get(); + + std::string message = formatTitle(title); + auto keyboard = KeyboardFactory::createTitleMenu(title.id); + + std::string imageUrl = "https://i.pinimg.com/736x/30/2b/49/302b49176e5a74ef43871a462df47e1f.jpg"; + + editMessageWithPhoto(chatId, messageId, imageUrl, {message, keyboard}); + + } catch (const std::exception& e) { + sendError(chatId, messageId, BotConstants::Text::SERVER_ERROR); + // Логирование ошибки (например, в cerr) + } + }); + + return; + /* case UserState::VIEWING_REVIEW: return returnReview(step.payload); // payload = reviewId case UserState::AWAITING_REVIEW: @@ -121,14 +140,14 @@ std::optional<NavigationStep> BotHandlers::computeNextStep( return NavigationStep{UserState::VIEWING_MY_TITLES, 0}; } break; - /* + case UserState::VIEWING_MY_TITLES: - if (data.starts_with("title_")) { + if (data.starts_with(BotConstants::Callback::CHOICE)) { int64_t titleId = parseId(data); return NavigationStep{UserState::VIEWING_TITLE_PAGE, titleId}; } break; - + /* case UserState::VIEWING_TITLE_PAGE: if (data == BotConstants::Callback::ACTION_ADD_REVIEW) { return NavigationStep{UserState::AWAITING_REVIEW, current.payload}; @@ -156,4 +175,45 @@ std::string BotHandlers::formatTitlesList(const std::vector<BotStructs::Title>& msg += std::to_string(i + 1) + ". " + titles[i].name + "\n"; } return msg; +} + +std::string BotHandlers::formatTitle(const BotStructs::Title& title) { + std::string msg; + msg += title.name + "\n"; + + return msg; +} + +int64_t BotHandlers::parseId(const std::string& data) { + const std::string prefix = "choice:"; + if (data.substr(0, prefix.size()) != prefix) { + throw std::invalid_argument("Data does not start with 'choice:'"); + } + + std::string idPart = data.substr(prefix.size()); + + // Проверяем, что остаток состоит только из цифр (и, возможно, знака '-') + if (idPart.empty()) { + throw std::invalid_argument("ID part is empty"); + } + + size_t startPos = 0; + if (idPart[0] == '-') { + if (idPart.size() == 1) { + throw std::invalid_argument("Invalid negative ID"); + } + startPos = 1; + } + + for (size_t i = startPos; i < idPart.size(); ++i) { + if (!std::isdigit(static_cast<unsigned char>(idPart[i]))) { + throw std::invalid_argument("ID contains non-digit characters"); + } + } + + try { + return std::stoll(idPart); + } catch (const std::out_of_range&) { + throw std::out_of_range("ID is out of range for int64_t"); + } } \ No newline at end of file diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 0555cc3..a6defe9 100644 --- a/modules/bot/front/src/handlers.cpp +++ b/modules/bot/front/src/handlers.cpp @@ -54,6 +54,9 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { if (data.starts_with(BotConstants::Callback::NAVIGATION)) { handleNavigation(query); } + else if (data.starts_with(BotConstants::Callback::CHOICE)) { + handleNavigation(query); + } else if (data.starts_with(BotConstants::Callback::ERROR)) { handleError(query); } @@ -89,14 +92,55 @@ void BotHandlers::increasePayload(int64_t& payload, const UserState curState) { } void BotHandlers::editMessage(int64_t chatId, int64_t messageId, HandlerResult response) { - botApi.editMessageText( - response.message, + // botApi.editMessageText быстрее. Реализовать его, где возможно + try { + botApi.deleteMessage(chatId, messageId); + } catch (const std::exception& e) { + std::cerr << "Warning: Failed to delete message " << messageId + << " in chat " << chatId << ": " << e.what() << std::endl; + // Продолжаем отправку нового сообщения даже при ошибке удаления + } + + // 2. Отправляем новое сообщение с правильным порядком параметров + botApi.sendMessage( + chatId, // chatId + response.message, // text + nullptr, // linkPreviewOptions (nullptr = default) + nullptr, // replyParameters (не отвечаем на сообщение) + response.keyboard, // replyMarkup — клавиатура + "HTML", // parseMode — поддержка <b>, <i> и т.д. + false // disableNotification + // остальные параметры по умолчанию + ); +} + +void BotHandlers::editMessageWithPhoto( + int64_t chatId, + int64_t messageId, + const std::string& imageUrl, + HandlerResult response) +{ + // 1. Удаляем старое сообщение + try { + botApi.deleteMessage(chatId, messageId); + } catch (const std::exception& e) { + // Игнорируем ошибку, если сообщение уже удалено или недоступно + std::cerr << "Warning: Failed to delete message: " << e.what() << std::endl; + } + + std::string safeCaption = response.message; + if (safeCaption.length() > 1024) { + safeCaption = safeCaption.substr(0, 1021) + "..."; + } + + // Отправка фото по URL + botApi.sendPhoto( chatId, - messageId, - "", - "", + imageUrl, + safeCaption, nullptr, - response.keyboard + response.keyboard, + "HTML" ); }