diff --git a/README.md b/README.md index 251c01a43..08d4104db 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ see [examples](examples/) for more: |:--------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------:| | [WeatherBot](examples/WeatherBot) | This example shows how to program a Telegram Bot that displays the weather information of a city using the [weather api](https://www.weatherapi.com/). | preview | | [EarthquakeBot](examples/EarthquakeBot) | This example shows how to program a Telegram Bot that will alert you if there is a recent earthquake somewhere in the world. | preview | +| [QrCodeBot](examples/QrCodeBot) | This example shows how to program a Telegram Bot that can generate QrCode images from text and extract text from QrCode Images. | preview | | [UrlShortenerBot](examples/UrlShortenerBot) | This example shows how to program Telegram Bot for shortening URLs. | preview | | [Buttons](examples/Buttons) | This example shows how to program a basic Telegram Bot that uses inline keyboard buttons to interact with users. | preview | diff --git a/examples/QrCodeBot/CMakeLists.txt b/examples/QrCodeBot/CMakeLists.txt new file mode 100644 index 000000000..714f259da --- /dev/null +++ b/examples/QrCodeBot/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.10) +project(qrcode_bot) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) +FetchContent_Declare(tgbotxx + GIT_REPOSITORY "https://github.com/baderouaich/tgbotxx" + GIT_TAG main + ) +FetchContent_MakeAvailable(tgbotxx) + +# Qr Code Processing Library +FetchContent_Declare(ZXing + GIT_REPOSITORY "https://github.com/zxing-cpp/zxing-cpp" + GIT_TAG master + ) +FetchContent_MakeAvailable(ZXing) + +# Stb libs for image read/write +FetchContent_Declare(stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master +) +FetchContent_MakeAvailable(stb) + +add_executable(${PROJECT_NAME} main.cpp) +target_link_libraries(${PROJECT_NAME} PUBLIC tgbotxx ZXing) +target_include_directories(${PROJECT_NAME} PUBLIC ${stb_SOURCE_DIR}) \ No newline at end of file diff --git a/examples/QrCodeBot/README.md b/examples/QrCodeBot/README.md new file mode 100644 index 000000000..55e9057d5 --- /dev/null +++ b/examples/QrCodeBot/README.md @@ -0,0 +1,22 @@ +## QR Code Bot +This example shows how to program a Telegram Bot that can generate QrCode images from text and extract text from QrCode Images. + +### Run +```bash +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make -j8 +./qrcode_bot YOUR_BOT_TOKEN +``` + +### How to create a new Bot and obtain its private token ? +1. Open the Telegram mobile app and search BotFather +2. Send BotFather a command /newbot +3. Follow instructions to create a new Bot +4. After you finish the instructions, you will receive a Bot Token, make sure you keep it secured. + +### Encode (Text to QRCode Image) +Encode + +### Decode (QRCode Image to Text) +Decode \ No newline at end of file diff --git a/examples/QrCodeBot/main.cpp b/examples/QrCodeBot/main.cpp new file mode 100644 index 000000000..5184c46d8 --- /dev/null +++ b/examples/QrCodeBot/main.cpp @@ -0,0 +1,149 @@ +#include // std::signal +#include +#include // tgbotxx +using namespace tgbotxx; + +#include +#include +#include +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +class QrCodeBot : public Bot { + public: + QrCodeBot(const std::string& token) : Bot(token) {} + + private: + void onStart() override { + // Drop pending updates + api()->deleteWebhook(true); + + // Register my commands + Ptr startCmd(new BotCommand()); + startCmd->command = "/start"; + startCmd->description = "Start interacting with the bot"; + api()->setMyCommands({startCmd}); + + std::cout << "Bot " << api()->getMe()->username << " Started\n"; + } + + void onStop() override { + std::cout << "\nStopping Bot. Please wait...\n"; + } + + void onNonCommandMessage(const Ptr& message) override try { + if (not message->photo.empty()) { // Did user send a photo ? + // Convert QrCode image to text + api()->sendChatAction(message->chat->id, "typing"); + std::string qrCodeText = extractTextFromQrCodeImage(message->photo); + api()->sendMessage(message->chat->id, qrCodeText); + } else if (not message->text.empty()) { // Did user send a text ? + // Convert text to QrCode image + api()->sendChatAction(message->chat->id, "upload_photo"); + cpr::File qrCodePhoto = convertTextToQrCodeImage(message->text); + api()->sendPhoto(message->chat->id, qrCodePhoto); + } + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + api()->sendMessage(message->chat->id, "Internal error"); + } + + void onCommand(const Ptr& message) override { + if (message->text == "/start") { + api()->sendMessage(message->chat->id, "Welcome to QrCodeBot! Please send a text to generate a QrCode Image, or send a QrCode Image to extract text from it."); + } + } + + protected: + /// Extracts UTF-8 text from a QR Code Image + /// @param photos multiple resolution photo sizes from Message + /// @returns Extracted UTF-8 text from the QR Code image + std::string extractTextFromQrCodeImage(const std::vector>& photos) { + // Get the highest resolution image (Telegram provides 4 resolutions of photo sent by user) + const Ptr& photo = *std::max_element(photos.begin(), photos.end(), [](const Ptr& A, const Ptr& B) { + return A->height < B->height && A->width < B->width; + }); + + // Download photo from Telegram servers + Ptr file = api()->getFile(photo->fileId); + std::string bytes = api()->downloadFile(file->filePath, [](long total, long downloaded) -> bool { + std::cout << "Downloading photo " << downloaded << '/' << total << " bytes \r" << std::flush; + return true; + }); + std::cout << std::endl; + + // Save downloaded photo to ./photos/input/PHOTO.jpg + fs::path photosDir = "photos/input"; + if (!fs::exists(photosDir)) fs::create_directory(photosDir); + fs::path photoPath = photosDir / fs::path(file->filePath).filename(); + std::ofstream ofs{photoPath, std::ios::binary}; + ofs.write(bytes.data(), bytes.size()); + ofs.close(); + bytes.clear(); + + // Load back image using stbi image + int width{}, height{}, channels{}; + std::unique_ptr buffer(stbi_load(photoPath.string().c_str(), &width, &height, &channels, 3), stbi_image_free); + if (!buffer) { + throw Exception("Failed to read image: " + photoPath.string()); + } + + // Decode qr code image + ZXing::DecodeHints hints{}; + hints.setTextMode(ZXing::TextMode::HRI); // Human Readable Interpretation + hints.setEanAddOnSymbol(ZXing::EanAddOnSymbol::Read); + hints.setTryHarder(true); + ZXing::ImageView image{buffer.get(), width, height, ZXing::ImageFormat::RGB}; + auto result = ZXing::ReadBarcode(image, hints); + if (result.isValid()) { + return result.text(); + } + return ZXing::ToString(result.error()); + } + + /// Converts @text to a QR Code image + /// @param text UTF-8 Text to convert + /// @returns cpr::File filename of the generated image + cpr::File convertTextToQrCodeImage(const std::string& text) { + ZXing::MultiFormatWriter writer(ZXing::BarcodeFormat::QRCode); + writer.setMargin(-1); + writer.setEncoding(ZXing::CharacterSet::UTF8); + ZXing::BitMatrix matrix = writer.encode(text, 128, 128); + ZXing::Matrix bitmap = ZXing::ToMatrix(matrix); + + fs::path photosDir = "photos/output"; + if (!fs::exists(photosDir)) fs::create_directories(photosDir); + fs::path photoPath = photosDir / (std::to_string(std::time(nullptr)) + ".jpg"); + // int success = stbi_write_png(filePath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); + int success = stbi_write_jpg(photoPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); + if (!success) { + throw Exception("Failed to write image: " + photoPath.string()); + } + return cpr::File(photoPath.string()); + } +}; + + +int main(int argc, const char *argv[]) { + if (argc < 2) { + std::cerr << "Usage:\nqrcode_bot \"BOT_TOKEN\"\n"; + return EXIT_FAILURE; + } + static std::unique_ptr BOT; + std::signal(SIGINT, [](int) { // Graceful Bot exit on CTRL+C + if (BOT) { + BOT->stop(); + } + std::exit(EXIT_SUCCESS); + }); + + BOT = std::make_unique(argv[1]); + BOT->start(); + return EXIT_SUCCESS; +}