From 847aec7bdd3816262b31ceaf22d79042e0346de0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 5 Dec 2025 12:38:34 +0300 Subject: [PATCH] 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 +#include +#include +#include +#include +#include + +using namespace org::openapitools::client::api; + +class BotToServer { +public: + BotToServer(); + + // Асинхронный метод: получить список тайтлов пользователя + pplx::task> fetchUserTitlesAsync(const std::string& userId); + +private: + std::shared_ptr apiconfiguration; + std::shared_ptr apiclient; + std::shared_ptr 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(); + 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(apiconfiguration); + api = std::make_shared(apiclient); +} + +// Вспомогательная функция: преобразует UserTitle → BotStructs::Title +static BotStructs::Title mapUserTitleToBotTitle( + const std::shared_ptr& 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> BotToServer::fetchUserTitlesAsync(const std::string& userId) { + utility::string_t userIdW = utility::conversions::to_string_t(userId); + int32_t limit = static_cast(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> task) { + try { + auto response = task.get(); + if (!response) { + throw std::runtime_error("Null response from getUserTitles"); + } + + const auto& userTitles = response->getData(); + std::vector result; + result.reserve(userTitles.size()); + + for (size_t i = 0; i < userTitles.size(); ++i) { + BotStructs::Title botTitle = mapUserTitleToBotTitle(userTitles[i]); + botTitle.num = static_cast(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 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