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 diff --git a/modules/bot/front/include/constants.hpp b/modules/bot/front/include/constants.hpp index 75e691e..e384186 100644 --- a/modules/bot/front/include/constants.hpp +++ b/modules/bot/front/include/constants.hpp @@ -3,6 +3,10 @@ #include namespace BotConstants { + 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 = "Найти аниме"; const std::string MY_TITLES = "Мои тайтлы"; @@ -16,13 +20,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 { diff --git a/modules/bot/front/include/handlers.hpp b/modules/bot/front/include/handlers.hpp index 7440d00..c3753b6 100644 --- a/modules/bot/front/include/handlers.hpp +++ b/modules/bot/front/include/handlers.hpp @@ -2,6 +2,7 @@ #include #include #include +#include /// @brief Структура возвращаемого значения класса BotHandlers для изменения текущего сообщения struct HandlerResult { @@ -9,6 +10,29 @@ 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 { + std::vector history; // Текущее состояние пользователя + история предыдущих состояний +}; + class BotHandlers { public: BotHandlers(TgBot::Api api) : botApi(api) {;} @@ -28,6 +52,7 @@ public: private: TgBot::Api botApi; + std::unordered_map userContexts; void handleNavigation(TgBot::CallbackQuery::Ptr query); @@ -37,4 +62,25 @@ 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); + + /// @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); }; diff --git a/modules/bot/front/src/handlers.cpp b/modules/bot/front/src/handlers.cpp index 4ae1040..a3633eb 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,101 @@ void BotHandlers::processCallbackImpl(TgBot::CallbackQuery::Ptr query) { 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()) { + // 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) { + 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) { + 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