π banana - thin wrapper over Telegram Bot API written in C++17.
- Simple API
- Single interface for both blocking, non-blocking and even coroutine-based operations
- Generic in terms of networking backend (bundled support for
WinAPI
,cpr
andboost::beast
) - Extendable (see custom-agent example)
- Automatically generated from Telegram Bot API 7.4 (thanks ark0f/tg-bot-api)
- Cross-platform (tested on Windows, Linux, macOS)
#include <banana/api.hpp>
#include <banana/agent/default.hpp>
int main(int argc, char** argv) {
// create agent once
banana::agent::default_blocking agent("<TG_BOT_TOKEN>");
// use API
banana::api::send_message(agent, { /* .chat_id = */ "@smertig", /* .text = */ "Hello, world!" });
}
TL;DR: use designated initialization from C++20 and forget about breaking changes.
Most of banana
is automatically generated from the Telegram Bot API documentation.
The main problem here is that there's no guarantee for the order of function parameters and members of types. Let's have a look at the following example:
struct send_message_args_t {
variant_t<integer_t, string_t> chat_id; // Unique identifier for the target chat or username of the target channel (in the format @channelusername)
string_t text; // Text of the message to be sent, 1-4096 characters after entities parsing
// ...
};
There are several similar ways to call send_message
:
// #1 (simple)
banana::api::send_message(agent, { "@username", "Hello from banana!" });
// #2 (designated initialization, C++20)
banana::api::send_message(agent, { .chat_id = "@username", .text = "Hello from banana!" });
// #3 (verbose)
banana::api::send_message_args_t args;
args.chat_id = "@username";
args.text = "Hello from banana!";
banana::api::send_message(agent, std::move(args));
However, they differ in the way they are affected by breaking changes. Nothing prevents the Telegram API team from updating send_message
parameters (and documentation) in the following way:
struct send_message_args_t {
variant_t<integer_t, string_t> chat_id;
string_t some_new_fancy_param; // This param is used only for a new shiny feature
string_t text;
// ...
};
How does this change affect existing code?
// #1 (simple) - silently breaks π£, sometimes compilation error β (in case of incompatible types)
banana::api::send_message(agent, { "@username", "Hello from banana!" });
// #2 (designated initialization, C++20) - fine π
banana::api::send_message(agent, { .chat_id = "@username", .text = "Hello from banana!" });
// #3 (verbose) - fine π
banana::api::send_message_args_t args;
args.chat_id = "@username";
args.text = "Hello from banana!";
That's why it's highly preferable to use C++20 designated initialization instead of a raw aggregate initialization.
There's also another case - when no new parameter is added, but existing parameters are swapped:
struct send_message_args_t {
string_t text;
variant_t<integer_t, string_t> chat_id;
// ...
};
This case is more tricky, because it can break designated initialization as well:
// #1 (simple) - silently breaks π£, sometimes compilation error β (in case of incompatible types)
banana::api::send_message(agent, { "@username", "Hello from banana!" });
// #2 (designated initialization, C++20) - compilation error because of incorrect designators order β
banana::api::send_message(agent, { .chat_id = "@username", .text = "Hello from banana!" });
// #3 (verbose) - fine π
banana::api::send_message_args_t args;
args.chat_id = "@username";
args.text = "Hello from banana!";
However, a compilation error is still much better than a silent change in logic, and also more readable than manual per-field initialization. That's why I recommend switching to C++20 and using designated initialization.