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)
This commit is contained in:
Kirill 2025-12-01 23:28:23 +03:00
parent 0fdf577612
commit ccf9722bb7
5 changed files with 145 additions and 44 deletions

View file

@ -33,5 +33,6 @@ namespace BotConstants {
namespace Text { namespace Text {
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 = "Проблемы с авторизацией, попробуйте авторизоваться повторно";
} }
} }

View file

@ -30,6 +30,7 @@ struct NavigationStep {
struct UserContext { struct UserContext {
int64_t userId;
std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний std::vector<NavigationStep> history; // Текущее состояние пользователя + история предыдущих состояний
}; };
@ -50,6 +51,10 @@ public:
/// @param message обрабатываемое сообщение /// @param message обрабатываемое сообщение
void handleMessage(TgBot::Message::Ptr message); void handleMessage(TgBot::Message::Ptr message);
/// @brief Создает контекст начального меню для пользователя
/// @param chatId id чата пользователя
void createInitContext(int64_t chatId);
private: private:
TgBot::Api botApi; TgBot::Api botApi;
std::unordered_map<int64_t, UserContext> userContexts; std::unordered_map<int64_t, UserContext> userContexts;
@ -60,8 +65,9 @@ private:
/// @brief Получить очередную страницу тайтлов из списка пользователя /// @brief Получить очередную страницу тайтлов из списка пользователя
/// @param userId Идентификатор пользователя /// @param userId Идентификатор пользователя
/// @param payload Полезная нагрузка
/// @return HandlerResult /// @return HandlerResult
static HandlerResult returnMyTitles(int64_t userId); static HandlerResult returnMyTitles(int64_t userId, int64_t payload);
/// @brief Вход в новое состояние /// @brief Вход в новое состояние
/// @param ctx текущий контекст /// @param ctx текущий контекст
@ -83,4 +89,33 @@ private:
/// @param payload Изменяемое значение нагрузки /// @param payload Изменяемое значение нагрузки
/// @param curState Текущее состояние /// @param curState Текущее состояние
void increasePayload(int64_t& payload, const UserState 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);
}; };

View file

@ -14,6 +14,7 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMainMenu() {
return keyboard; return keyboard;
} }
// TODO: Переписать с учетом констант на количество отображаемых тайтлов и нового callback'a
TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Title> titles) { TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<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;
@ -39,13 +40,23 @@ TgBot::InlineKeyboardMarkup::Ptr KeyboardFactory::createMyTitles(std::vector<Tit
if(counter % 2 == 1) { if(counter % 2 == 1) {
auto button = std::make_shared<TgBot::InlineKeyboardButton>(); auto button = std::make_shared<TgBot::InlineKeyboardButton>();
button->text = BotConstants::Button::PREV; 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); layout[counter / 2].push_back(button);
} }
else { else {
auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>(); auto button_prev = std::make_shared<TgBot::InlineKeyboardButton>();
button_prev->text = BotConstants::Button::PREV; 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>(); auto button_next = std::make_shared<TgBot::InlineKeyboardButton>();
button_next->text = BotConstants::Button::NEXT; 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);

View file

@ -12,6 +12,8 @@ AnimeBot::AnimeBot(const std::string& token)
void AnimeBot::setupHandlers() { void AnimeBot::setupHandlers() {
bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) { bot.getEvents().onCommand("start", [this](TgBot::Message::Ptr message) {
sendMainMenu(message->chat->id); sendMainMenu(message->chat->id);
//TODO: производить инициализацию контекста только после авторизации
handler.createInitContext(message->chat->id);
}); });
bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) { bot.getEvents().onCallbackQuery([this](TgBot::CallbackQuery::Ptr query) {

View file

@ -20,7 +20,7 @@ void BotHandlers::handleCallback(TgBot::CallbackQuery::Ptr query) {
processCallbackImpl(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}}; std::vector<Title> titles = {{123, "Школа мертвяков", "", 1}, {321, "KissXsis", "", 2}};
@ -31,44 +31,6 @@ HandlerResult BotHandlers::returnMyTitles(int64_t userId) {
return result; 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) { void BotHandlers::handleMessage(TgBot::Message::Ptr message) {
//TODO: просмотр состояния пользователя //TODO: просмотр состояния пользователя
return; return;
@ -90,6 +52,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) {
auto it = userContexts.find(userId); auto it = userContexts.find(userId);
if (it == userContexts.end()) { if (it == userContexts.end()) {
// TODO: log // TODO: log
sendError(query, BotConstants::Text::AUTH_ERROR);
std::cout << "Error: Не нашел пользователя " << userId; std::cout << "Error: Не нашел пользователя " << userId;
return; return;
} }
@ -127,7 +90,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) {
// Обработка back по интерфейсу // Обработка back по интерфейсу
if (data == BotConstants::Callback::NAV_BACK) { if (data == BotConstants::Callback::NAV_BACK) {
if (!popState(ctx)) { if (!popState(ctx)) {
botApi.answerCallbackQuery(query->id, "Некуда возвращаться", true); sendError(query, BotConstants::Text::SAD_ERROR);
return; return;
} }
auto result = renderCurrent(ctx); auto result = renderCurrent(ctx);
@ -138,7 +101,7 @@ void BotHandlers::handleNavigation(TgBot::CallbackQuery::Ptr query) {
// Переходы вперёд (push) // Переходы вперёд (push)
auto newStepOpt = computeNextStep(query, current); auto newStepOpt = computeNextStep(query, current);
if (!newStepOpt.has_value()) { if (!newStepOpt.has_value()) {
sendError(query->message->chat->id); sendError(query, BotConstants::Text::SAD_ERROR);
return; return;
} }
@ -181,4 +144,93 @@ void BotHandlers::increasePayload(int64_t& payload, const UserState curState) {
payload = BotConstants::NULL_PAYLOAD; payload = BotConstants::NULL_PAYLOAD;
std::cerr << "Error: increasePayload" << std::endl; 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}};
} }