Skip to content

Commit

Permalink
Allow user to set custom long polling timeout
Browse files Browse the repository at this point in the history
api requests and file upload download timeouts.
  • Loading branch information
baderouaich committed Nov 4, 2023
1 parent 955ac87 commit 19648ba
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 26 deletions.
48 changes: 41 additions & 7 deletions include/tgbotxx/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ namespace tgbotxx {
/// @note All methods in the Bot API are case-insensitive.
/// @note We support GET and POST HTTP methods. Use either URL query string or application/json or application/x-www-form-urlencoded or multipart/form-data for passing parameters in Bot API requests.
class Api {
static const std::string BASE_URL; /// Telegram api base url
static const cpr::Timeout TIMEOUT; /// 25s (Telegram server can take up to 25s to reply us (should be longer than long poll timeout)). Max long polling timeout seems to be 50s.
static const cpr::Timeout FILES_UPLOAD_TIMEOUT; /// 5min (Files can take longer time to upload. Setting a shorter timeout can stop the request even if the file isn't fully uploaded)
static const cpr::ConnectTimeout CONNECT_TIMEOUT; /// 20s (Telegram server can take up to 20s to connect with us)
static const std::int32_t LONG_POLL_TIMEOUT; /// 10s (calling getUpdates() every 10 seconds)
const std::string m_token; /// Bot token from @BotFather
static const std::string BASE_URL; /// Telegram api base url
static const cpr::ConnectTimeout DEFAULT_CONNECT_TIMEOUT; /// 20s (Telegram server can take up to 20s to connect with us)
static const cpr::Timeout DEFAULT_TIMEOUT; /// 70s (Telegram server can take up to 70s to reply us (should be longer than long poll timeout)).
static const cpr::Timeout DEFAULT_LONG_POLL_TIMEOUT; /// 60s (long polling getUpdates() every 30 seconds) Telegram's guidelines recommended a timeout between 30 and 90 seconds for long polling.
static const cpr::Timeout DEFAULT_UPLOAD_FILES_TIMEOUT; /// 15min (Files can take longer time to upload. Setting a shorter timeout will stop the request even if the file isn't fully uploaded)
static const cpr::Timeout DEFAULT_DOWNLOAD_FILES_TIMEOUT; /// 30min (Files can take longer time to download. Setting a shorter timeout will stop the request even if the file isn't fully downloaded)

const std::string m_token; /// Bot token from \@BotFather
cpr::ConnectTimeout m_connectTimeout = DEFAULT_CONNECT_TIMEOUT; /// Api connection timeout
cpr::Timeout m_timeout = DEFAULT_TIMEOUT; /// Api requests timeout
cpr::Timeout m_longPollTimeout = DEFAULT_LONG_POLL_TIMEOUT; /// Long polling timeout
cpr::Timeout m_uploadFilesTimeout = DEFAULT_UPLOAD_FILES_TIMEOUT; /// Api files upload timeout
cpr::Timeout m_downloadFilesTimeout = DEFAULT_DOWNLOAD_FILES_TIMEOUT; /// Api files download timeout

public:
/// @brief Constructs Api object.
Expand Down Expand Up @@ -1394,7 +1401,7 @@ namespace tgbotxx {
/// @note This method will not work if an outgoing webhook is set up.
/// @note In order to avoid getting duplicate updates, recalculate offset after each server response.
/// @link ref https://core.telegram.org/bots/api#getupdates @endlink
std::vector<Ptr<Update>> getUpdates(std::int32_t offset, std::int32_t limit = 100, std::int32_t timeout = LONG_POLL_TIMEOUT, const std::vector<std::string>& allowedUpdates = {}) const;
std::vector<Ptr<Update>> getUpdates(std::int32_t offset, std::int32_t limit = 100, const std::vector<std::string>& allowedUpdates = {}) const;

/// @brief Use this method to specify a URL and receive incoming updates via an outgoing webhook.
/// Whenever there is an update for the bot, we will send an HTTPS POST request to the specified URL, containing a JSON-serialized Update.
Expand Down Expand Up @@ -1467,6 +1474,33 @@ namespace tgbotxx {
const std::string& nextOffset = "",
const Ptr<InlineQueryResultsButton>& button = nullptr) const;


public: /// Timeout setters/getters
/// @brief Set long polling timeout.
void setLongPollTimeout(const cpr::Timeout& longPollTimeout);
/// @brief Get long polling timeout.
cpr::Timeout getLongPollTimeout() const noexcept;

/// @brief Set Api requests connection timeout.
void setConnectTimeout(const cpr::ConnectTimeout& timeout) noexcept;
/// @brief Get Api requests connection timeout.
cpr::ConnectTimeout getConnectTimeout() const noexcept;

/// @brief Set Api requests timeout.
void setTimeout(const cpr::Timeout& timeout);
/// @brief Get Api requests timeout.
cpr::Timeout getTimeout() const noexcept;

/// @brief Set Api file uploads timeout.
void setUploadFilesTimeout(const cpr::Timeout& timeout) noexcept;
/// @brief Get Api file uploads timeout.
cpr::Timeout getUploadFilesTimeout() const noexcept;

/// @brief Set Api file downloads timeout.
void setDownloadFilesTimeout(const cpr::Timeout& timeout) noexcept;
/// @brief Get APi file downloads timeout.
cpr::Timeout getDownloadFilesTimeout() const noexcept;

private:
nl::json sendRequest(const std::string& endpoint, const cpr::Multipart& data = cpr::Multipart({})) const;
};
Expand Down
63 changes: 46 additions & 17 deletions src/Api.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include "tgbotxx/Exception.hpp"
#include <chrono>
#include <tgbotxx/Api.hpp>
/// Objects
#include <tgbotxx/objects/Animation.hpp>
Expand Down Expand Up @@ -90,16 +92,16 @@
#include <tgbotxx/objects/WebhookInfo.hpp>
#include <tgbotxx/objects/WriteAccessAllowed.hpp>


#include <utility>
using namespace tgbotxx;

/// Static declarations
const std::string Api::BASE_URL = "https://api.telegram.org"; /// Telegram api base url
const cpr::Timeout Api::TIMEOUT = 25 * 1000; /// 25s (Telegram server can take up to 25s to reply us (should be longer than long poll timeout)). Max long polling timeout seems to be 50s.
const cpr::Timeout Api::FILES_UPLOAD_TIMEOUT = 300 * 1000; /// 5min (Files can take longer time to upload. Setting a shorter timeout can stop the request even if the file isn't fully uploaded)
const cpr::ConnectTimeout Api::CONNECT_TIMEOUT = 20 * 1000; /// 20s (Telegram server can take up to 20s to connect with us)
const std::int32_t Api::LONG_POLL_TIMEOUT = 10; /// 10s (calling getUpdates() every 10 seconds)
const std::string Api::BASE_URL = "https://api.telegram.org"; /// Telegram api base url
const cpr::ConnectTimeout Api::DEFAULT_CONNECT_TIMEOUT = std::chrono::milliseconds(20 * 1000); /// 20s (Telegram server can take up to 20s to connect with us)
const cpr::Timeout Api::DEFAULT_TIMEOUT = std::chrono::seconds(60 + 10); /// 70s (Telegram server can take up to 70s to reply us (should be longer than long poll timeout)).
const cpr::Timeout Api::DEFAULT_LONG_POLL_TIMEOUT = std::chrono::seconds(60); /// 60s (long polling getUpdates() every 30 seconds) Telegram's guidelines recommended a timeout between 30 and 90 seconds for long polling.
const cpr::Timeout Api::DEFAULT_UPLOAD_FILES_TIMEOUT = std::chrono::seconds(15 * 60); /// 15min (Files can take longer time to upload. Setting a shorter timeout will stop the request even if the file isn't fully uploaded)
const cpr::Timeout Api::DEFAULT_DOWNLOAD_FILES_TIMEOUT = std::chrono::seconds(30 * 60); /// 30min (Files can take longer time to download. Setting a shorter timeout will stop the request even if the file isn't fully downloaded)

Api::Api(const std::string& token) : m_token(token) {}

Expand All @@ -108,11 +110,8 @@ nl::json Api::sendRequest(const std::string& endpoint, const cpr::Multipart& dat
// You can initiate multiple concurrent requests to the Telegram API, which means
// You can call sendMessage while getUpdates long polling is still pending, and you can't do that with a single cpr::Session instance.
bool hasFiles = std::any_of(data.parts.begin(), data.parts.end(), [](const cpr::Part& part) noexcept { return part.is_file; });
if (hasFiles) // Files can take longer to upload
session.SetTimeout(FILES_UPLOAD_TIMEOUT);
else
session.SetTimeout(TIMEOUT);
session.SetConnectTimeout(CONNECT_TIMEOUT);
session.SetConnectTimeout(m_connectTimeout);
session.SetTimeout(hasFiles ? m_uploadFilesTimeout : m_timeout); // Files can take longer to upload
session.SetHeader(cpr::Header{
{"Connection", "close"}, // disable keep-alive
{"Accept", "application/json"},
Expand Down Expand Up @@ -442,13 +441,13 @@ Ptr<File> Api::getFile(const std::string& fileId) const {
}

std::string Api::downloadFile(const std::string& filePath, const std::function<bool(cpr::cpr_off_t, cpr::cpr_off_t)>& progressCallback) const {
std::ostringstream oss;
std::ostringstream oss{};
oss << BASE_URL << "/file/bot" << m_token << "/" << filePath;

cpr::Session session{};
session.SetUrl(cpr::Url{oss.str()});
session.SetTimeout(FILES_UPLOAD_TIMEOUT);
session.SetConnectTimeout(CONNECT_TIMEOUT);
session.SetConnectTimeout(m_connectTimeout);
session.SetTimeout(m_downloadFilesTimeout);
session.SetAcceptEncoding({cpr::AcceptEncodingMethods::deflate, cpr::AcceptEncodingMethods::gzip, cpr::AcceptEncodingMethods::zlib});
if (progressCallback) {
cpr::ProgressCallback pCallback{[&progressCallback](cpr::cpr_off_t downloadTotal,
Expand Down Expand Up @@ -1681,13 +1680,12 @@ bool Api::deleteWebhook(bool dropPendingUpdates) const {
}

/// Called every LONG_POOL_TIMEOUT seconds
std::vector<Ptr<Update>> Api::getUpdates(std::int32_t offset, std::int32_t limit, std::int32_t timeout,
const std::vector<std::string>& allowedUpdates) const {
std::vector<Ptr<Update>> Api::getUpdates(std::int32_t offset, std::int32_t limit, const std::vector<std::string>& allowedUpdates) const {
std::vector<Ptr<Update>> updates;
cpr::Multipart data = {
{"offset", offset},
{"limit", std::max<std::int32_t>(1, std::min<std::int32_t>(100, limit))},
{"timeout", timeout},
{"timeout", static_cast<std::int32_t>(std::chrono::duration_cast<std::chrono::seconds>(m_longPollTimeout.ms).count())},
{"allowed_updates", nl::json(allowedUpdates).dump()},
};
nl::json updatesJson = sendRequest("getUpdates", data);
Expand Down Expand Up @@ -1757,3 +1755,34 @@ bool Api::answerInlineQuery(const std::string& inlineQueryId,

return sendRequest("answerInlineQuery", data);
}

void Api::setLongPollTimeout(const cpr::Timeout& longPollTimeout) {
if (longPollTimeout.ms > m_timeout.ms)
throw Exception("Api::setLongPollTimeout: Long poll timeout should always be shorter than api request timeout."
" Otherwise the api request will time out before long polling finishes.");
m_longPollTimeout = longPollTimeout;
}
cpr::Timeout Api::getLongPollTimeout() const noexcept { return m_longPollTimeout; }

void Api::setConnectTimeout(const cpr::ConnectTimeout& timeout) noexcept {
m_connectTimeout = timeout;
}
cpr::ConnectTimeout Api::getConnectTimeout() const noexcept { return m_connectTimeout; }

void Api::setTimeout(const cpr::Timeout& timeout) {
if (timeout.ms <= m_longPollTimeout.ms)
throw Exception("Api::setTimeout: Api request timeout should always be longer than long poll timeout."
" Otherwise the api request will time out before long polling finishes.");
m_timeout = timeout;
}
cpr::Timeout Api::getTimeout() const noexcept { return m_timeout; }

void Api::setUploadFilesTimeout(const cpr::Timeout& timeout) noexcept {
m_uploadFilesTimeout = timeout;
}
cpr::Timeout Api::getUploadFilesTimeout() const noexcept { return m_uploadFilesTimeout; }

void Api::setDownloadFilesTimeout(const cpr::Timeout& timeout) noexcept {
m_downloadFilesTimeout = timeout;
}
cpr::Timeout Api::getDownloadFilesTimeout() const noexcept { return m_downloadFilesTimeout; }
5 changes: 3 additions & 2 deletions tests/manual_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#include "tgbotxx/objects/WebAppInfo.hpp"
#include "tgbotxx/objects/WebhookInfo.hpp"
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <tgbotxx/tgbotxx.hpp>
Expand All @@ -20,10 +19,11 @@ class MyBot : public Bot {
/// Called before Bot starts receiving updates (triggered by Bot::start())
/// Use this callback to initialize your code, set commands..
void onStart() override {
// api()->setTimeout(std::chrono::seconds(60 * 3));
// api()->setLongPollTimeout(std::chrono::seconds(60 * 2));
// Drop awaiting updates (when Bot is not running, updates will remain 24 hours
// in Telegram server before they get deleted or retrieved by BOT)
getApi()->deleteWebhook(true);

// api()->setMyName("tgbotxx manual_tests");
// api()->setMyDescription("tgbotxx bot manual tests");

Expand Down Expand Up @@ -336,6 +336,7 @@ int main() try {
std::signal(SIGINT, [](int) {
std::cout << "Stopping Bot. Please wait...\n";
bot.stop();
std::exit(EXIT_SUCCESS);
});
bot.start();
return EXIT_SUCCESS;
Expand Down
20 changes: 20 additions & 0 deletions tests/src/TestApi.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include <chrono>
#define CATCH_CONFIG_MAIN
#include <tgbotxx/tgbotxx.hpp>
#include <catch2/catch.hpp>
using namespace tgbotxx;

thread_local static Ptr<Api> API(new Api(std::getenv("TESTS_BOT_TOKEN") ?: "BOT_TOKEN"));


TEST_CASE("Test Api", "methods")
{
SECTION("getMe") {
Expand All @@ -22,4 +24,22 @@ TEST_CASE("Test Api", "methods")
}


SECTION("timeouts") {
cpr::Timeout longPollTimeout = std::chrono::seconds(60);
cpr::Timeout timeout = std::chrono::seconds(70); // Must be longer than long polling timeout
API->setLongPollTimeout(longPollTimeout);
API->setTimeout(timeout);

// Getters & setters
REQUIRE(timeout.ms == API->getTimeout().ms);
REQUIRE(longPollTimeout.ms == API->getLongPollTimeout().ms);

// Try set a timeout that is less than long polling timeout
REQUIRE_THROWS(API->setTimeout(std::chrono::seconds(30)));
// .. or vice versa
REQUIRE_THROWS(API->setLongPollTimeout(std::chrono::seconds(80)));
REQUIRE_THROWS_AS(API->setLongPollTimeout(std::chrono::seconds(80)), Exception);

}

}

0 comments on commit 19648ba

Please sign in to comment.