#include "telegram_client.hpp"
#include <algorithm>
#include <chrono>
#include <cctype>
#include <format>
#include <optional>
#include <string_view>
#include <mw/http_client.hpp>
#include <mw/url.hpp>
#include "service_error.hpp"
namespace telegrammer
{
namespace
{
std::string lower(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char character)
{
return static_cast<char>(std::tolower(character));
});
return value;
}
std::optional<int> retryAfter(const json& body)
{
if(!body.is_object() || !body.contains("parameters") ||
!body["parameters"].is_object() ||
!body["parameters"].contains("retry_after") ||
!body["parameters"]["retry_after"].is_number_integer())
{
return std::nullopt;
}
int64_t value = 0;
try
{
value = body["parameters"]["retry_after"].get<int64_t>();
}
catch(const std::exception&)
{
return std::nullopt;
}
if(value <= 0 || value > 3600)
{
return std::nullopt;
}
return static_cast<int>(value);
}
mw::Error telegramFailure(int status, const json& body)
{
int error_code = 0;
if(body.is_object() && body.contains("error_code") &&
body["error_code"].is_number_integer())
{
try
{
error_code = body["error_code"].get<int>();
}
catch(const std::exception&)
{
error_code = 0;
}
}
std::string description = "Telegram rejected the request";
if(body.is_object() && body.contains("description") &&
body["description"].is_string())
{
description = body["description"].get<std::string>();
}
std::optional<int> retry_after = retryAfter(body);
if(status == 429 || error_code == 429 || retry_after.has_value())
{
return serviceError(429, "TELEGRAM_RATE_LIMITED", description,
retry_after, true);
}
if(status == 400 || status == 403 || error_code == 400 ||
error_code == 403)
{
return serviceError(422, "TELEGRAM_REJECTED", description);
}
if(status == 401 || error_code == 401)
{
return serviceError(502, "UPSTREAM_ERROR", description);
}
return serviceError(502, "UPSTREAM_ERROR", description,
std::nullopt, status >= 500);
}
bool looksLikeTimeout(const std::string& message)
{
std::string value = lower(message);
return value.find("timeout") != std::string::npos ||
value.find("timed out") != std::string::npos;
}
} // namespace
TelegramClient::TelegramClient(std::string token)
: token_(std::move(token)),
base_url_(std::format("https://api.telegram.org/bot{}/", token_))
{}
mw::E<json> TelegramClient::request(const std::string& method,
const std::string& payload,
bool post) const
{
mw::HTTPSession session;
auto connection_result = session.connectionTimeout(
std::chrono::seconds(5));
auto transfer_result = session.transferTimeout(
std::chrono::seconds(post ? 10 : 40));
auto size_result = session.maxSize(16 * 1024 * 1024);
auto protocol_result = session.allowedProtocols("https");
session.followRedirects(false);
if(!connection_result.has_value() || !transfer_result.has_value() ||
!size_result.has_value() || !protocol_result.has_value())
{
return std::unexpected(serviceError(
500, "HTTP_CLIENT_CONFIGURATION", "Unable to configure HTTP"));
}
mw::HTTPResponse const* response = nullptr;
if(post)
{
mw::HTTPRequest request(base_url_ + method);
request.setContentType("application/json");
request.setPayload(payload);
auto result = session.post(request);
if(!result.has_value())
{
std::string message = mw::errorMsg(result.error());
if(looksLikeTimeout(message))
{
return std::unexpected(serviceError(
504, "UPSTREAM_TIMEOUT", "Telegram request timed out",
std::nullopt, true));
}
return std::unexpected(serviceError(
502, "UPSTREAM_UNAVAILABLE", "Telegram is unavailable",
std::nullopt, true));
}
response = *result;
}
else
{
auto result = session.get(base_url_ + method + payload);
if(!result.has_value())
{
std::string message = mw::errorMsg(result.error());
if(looksLikeTimeout(message))
{
return std::unexpected(serviceError(
504, "UPSTREAM_TIMEOUT", "Telegram request timed out",
std::nullopt, true));
}
return std::unexpected(serviceError(
502, "UPSTREAM_UNAVAILABLE", "Telegram is unavailable",
std::nullopt, true));
}
response = *result;
}
json body;
try
{
body = json::parse(response->payloadAsStr());
}
catch(const std::exception&)
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned invalid JSON"));
}
bool successful = response->status >= 200 && response->status < 300 &&
body.is_object() && body.contains("ok") &&
body["ok"].is_boolean() && body["ok"].get<bool>();
if(!successful)
{
return std::unexpected(telegramFailure(response->status, body));
}
if(!body.contains("result"))
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned an incomplete response"));
}
return body;
}
mw::E<json> TelegramClient::getMe() const
{
auto result = request("getMe", "", false);
if(!result.has_value())
{
return std::unexpected(result.error());
}
if(!(*result)["result"].is_object() ||
!(*result)["result"].contains("id") ||
!(*result)["result"]["id"].is_number_integer())
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned an invalid bot identity"));
}
try
{
[[maybe_unused]] auto bot_id =
(*result)["result"]["id"].get<int64_t>();
}
catch(const std::exception&)
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned an invalid bot identity"));
}
return *result;
}
mw::E<json> TelegramClient::sendMessage(int64_t chat_id,
const std::string& text) const
{
auto result = request("sendMessage", json{{"chat_id", chat_id},
{"text", text}}
.dump(),
true);
if(!result.has_value())
{
return std::unexpected(result.error());
}
if(!(*result)["result"].is_object())
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned an invalid message"));
}
return *result;
}
mw::E<json> TelegramClient::getUpdates(int64_t offset, int timeout) const
{
std::string query = std::format(
"?offset={}&timeout={}&allowed_updates={}", offset, timeout,
mw::URL::encode("[\"message\"]"));
auto result = request("getUpdates", query, false);
if(!result.has_value())
{
return std::unexpected(result.error());
}
if(!(*result)["result"].is_array())
{
return std::unexpected(serviceError(
502, "UPSTREAM_ERROR", "Telegram returned invalid updates"));
}
return *result;
}
} // namespace telegrammer