feat(tgbot): add title menu
This commit is contained in:
parent
f045eb22b2
commit
da40d7df49
7 changed files with 244 additions and 11 deletions
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ struct Title {
|
|||
std::string name;
|
||||
std::string description;
|
||||
int64_t num;
|
||||
std::string imageUrl;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue