feat(tgbot): add title menu

This commit is contained in:
Kirill 2025-12-20 01:19:56 +03:00
parent f045eb22b2
commit da40d7df49
7 changed files with 244 additions and 11 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -6,5 +6,6 @@ struct Title {
std::string name;
std::string description;
int64_t num;
std::string imageUrl;
};
}

View file

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

View file

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