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
This commit is contained in:
Kirill 2025-12-05 12:38:34 +03:00
parent 4ca8b19adb
commit 847aec7bdd
9 changed files with 190 additions and 13 deletions

View file

@ -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;
};

View file

@ -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;
}
});
}

View file

@ -1,7 +1,16 @@
cmake_minimum_required(VERSION 3.10.2) cmake_minimum_required(VERSION 3.10.2)
project(AnimeBot) 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 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
@ -12,8 +21,11 @@ find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
find_package(Boost COMPONENTS system REQUIRED) find_package(Boost COMPONENTS system REQUIRED)
find_package(CURL) find_package(CURL)
find_library(CPPREST_LIB cpprest REQUIRED)
include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR}) include_directories(/usr/local/include ${OPENSSL_INCLUDE_DIR} ${Boost_INCLUDE_DIR})
include_directories(include/) include_directories(include/)
include_directories(../back/include)
include_directories(../generated-client/include)
if (CURL_FOUND) if (CURL_FOUND)
include_directories(${CURL_INCLUDE_DIRS}) include_directories(${CURL_INCLUDE_DIRS})
add_definitions(-DHAVE_CURL) add_definitions(-DHAVE_CURL)
@ -21,4 +33,4 @@ endif()
add_executable(AnimeBot ${SOURCES}) 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})

View file

@ -7,7 +7,7 @@ public:
static TgBot::InlineKeyboardMarkup::Ptr createMainMenu(); static TgBot::InlineKeyboardMarkup::Ptr createMainMenu();
/// Create keyboard for My_Titles /// 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 /// Create keyboard for sendError
static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback); static TgBot::InlineKeyboardMarkup::Ptr createError(const std::string& errorCallback);

View file

@ -39,5 +39,6 @@ namespace BotConstants {
const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?"; const std::string MAIN_MENU = "Вас приветствует nyanimedb бот:)\nЧего будем делать?";
const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся"; const std::string SAD_ERROR = "У нас что-то случилось:(\nМы обязательно скоро исправимся";
const std::string AUTH_ERROR = "Проблемы с авторизацией, попробуйте авторизоваться повторно"; const std::string AUTH_ERROR = "Проблемы с авторизацией, попробуйте авторизоваться повторно";
const std::string SERVER_ERROR = "Не удалось загрузить данные. Попробуйте позже.";
} }
} }

View file

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <structs.hpp> #include <structs.hpp>
#include <unordered_map> #include <unordered_map>
#include "BotToServer.hpp"
/// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения /// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения
struct HandlerResult { struct HandlerResult {
@ -58,6 +59,7 @@ public:
private: private:
TgBot::Api botApi; TgBot::Api botApi;
std::unordered_map<int64_t, UserContext> userContexts; std::unordered_map<int64_t, UserContext> userContexts;
BotToServer server_;
void handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx); void handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext& ctx);
@ -104,7 +106,7 @@ private:
/// @brief Отрисовка текущего экрана (соотв. контексту) /// @brief Отрисовка текущего экрана (соотв. контексту)
/// @param ctx - текущий контекст /// @param ctx - текущий контекст
/// @return HandlerResult для нового состояния сообщения /// @return HandlerResult для нового состояния сообщения
HandlerResult renderCurrent(const UserContext& ctx); HandlerResult renderCurrent(TgBot::CallbackQuery::Ptr query, const UserContext& ctx);
/// @brief Логика переходов между контекстами (навигация на следующий шаг) /// @brief Логика переходов между контекстами (навигация на следующий шаг)
/// @param query - запрос /// @param query - запрос
@ -120,4 +122,7 @@ private:
/// @brief Посылает интерфейс обработки ошибки на callback запрос /// @brief Посылает интерфейс обработки ошибки на callback запрос
/// @param query запрос /// @param query запрос
void sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText); void sendError(TgBot::CallbackQuery::Ptr query, const std::string& errText);
// Форматирование для отображения в сообщении
std::string formatTitlesList(const std::vector<BotStructs::Title>& titles);
}; };

View file

@ -1,8 +1,10 @@
#pragma once #pragma once
namespace BotStructs {
struct Title { struct Title {
int64_t id; int64_t id;
std::string name; std::string name;
std::string description; std::string description;
int64_t num; int64_t num;
}; };
}

View file

@ -15,13 +15,13 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() {
} }
// TODO: Переписать с учетом констант на количество отображаемых тайтлов и нового callback'a // 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>(); auto keyboard = std::make_shared<TgBot::InlineKeyboardMarkup>();
std::vector<TgBot::InlineKeyboardButton::Ptr> row; std::vector<TgBot::InlineKeyboardButton::Ptr> row;
std::vector<std::vector<TgBot::InlineKeyboardButton::Ptr>> layout; std::vector<std::vector<TgBot::InlineKeyboardButton::Ptr>> layout;
int counter = 0; int counter = 0;
for(Title& title : titles) { for(BotStructs::Title& title : titles) {
if(counter >= 6) { if(counter >= 6) {
break; break;
} }

View file

@ -22,7 +22,7 @@ void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) {
HandlerResult BotHandlers::returnMyTitles(int64_t userId, int64_t payload) { 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; struct HandlerResult result;
result.keyboard = KeyboardFactory::createMyTitles(titles); result.keyboard = KeyboardFactory::createMyTitles(titles);
@ -85,7 +85,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext&
ctx.history.back().payload = newPayload; ctx.history.back().payload = newPayload;
auto result = renderCurrent(ctx); auto result = renderCurrent(query, ctx);
if(result.message == "meow") return; // TODO: убрать
editMessage(query, result); editMessage(query, result);
return; return;
} }
@ -96,7 +97,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext&
sendError(query, BotConstants::Text::SAD_ERROR); sendError(query, BotConstants::Text::SAD_ERROR);
return; return;
} }
auto result = renderCurrent(ctx); auto result = renderCurrent(query, ctx);
if(result.message == "meow") return; // TODO: убрать
editMessage(query, result); editMessage(query, result);
return; return;
} }
@ -109,7 +111,8 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query, UserContext&
} }
ctx.history.push_back(*newStepOpt); ctx.history.push_back(*newStepOpt);
auto result = renderCurrent(ctx); auto result = renderCurrent(query, ctx);
if(result.message == "meow") return; // TODO: убрать
editMessage(query, result); editMessage(query, result);
} }
@ -161,13 +164,30 @@ 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(); const auto& step = ctx.history.back();
int64_t userId = query->from->id;
switch (step.state) { switch (step.state) {
case UserState::MAIN_MENU: case UserState::MAIN_MENU:
return showMainMenu(); return showMainMenu();
case UserState::VIEWING_MY_TITLES: 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 return returnTitlePage(step.payload); // payload = titleId
@ -257,3 +277,16 @@ void BotHandlers::handleError(TgBot::CallbackQuery::Ptr query, UserContext& ctx)
editMessage(query, result); 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;
}