Skip to content

Commit

Permalink
Added PaidSubscriptionBot example
Browse files Browse the repository at this point in the history
  • Loading branch information
baderouaich committed Dec 27, 2023
1 parent 5c5abb0 commit da20a7c
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 0 deletions.
16 changes: 16 additions & 0 deletions examples/PaidSubscriptionBot/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.10)
project(paid_subscription_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)

add_executable(${PROJECT_NAME} src/main.cpp src/PaidSubscriptionBot.cpp)
target_link_libraries(${PROJECT_NAME} PUBLIC tgbotxx)

76 changes: 76 additions & 0 deletions examples/PaidSubscriptionBot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## Paid subscription bot using Payments api methods
This example shows how to program a Telegram Bot that charges for its services using the [api payments methods](https://core.telegram.org/bots/api#payments).

### Run
```bash
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8
./paid_subscription_bot YOUR_BOT_TOKEN YOUR_PAYMENT_PROVIDER_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.


### How to obtain a payment provider token ?
1. Open the Telegram mobile app and search BotFather
2. Send BotFather a command /mybots
3. Click your bot
4. Click Payments
5. Choose your payment provider and follow instructions to get your payment provider token, make sure you keep it secured.


### How to test an invoice payment ?
Never use real money to test your Bot invoice payments. Telegram has a testing card which you can use to pay for your testing invoices:<br>
see [Testing payments](https://core.telegram.org/bots/payments#testing-payments-the-39stripe-test-mode-39-provider)<br>
Simply use card info: <br>
**Card number** `42 42 42 42 42 42 42 42`<br>
**CVV** `any`<br>
**Expiration Date** `any`

Using this testing card will perform an invoice payment callback to your bot and will trigger the `Bot::onPreCheckoutQuery`, then you will have to
answer to the query in less than 10s with `Api::answerPreCheckoutQuery` to confirm or deny the payment.

### Flowchart
```mermaid
flowchart TD
User -->|/subscribe| Bot
Bot --> C{User subscribed already?}
C -->|Yes| D[Bot::Ignore]
C -->|No| E[Bot::subscribeNewUser]
E --> Api::sendInvoice --> User
User --> PaysForInvoice
PaysForInvoice-->Bot::onPreCheckoutQuery-->Bot
Bot-->K{Approve or deny payment}
K--> |Approve| F[Api::answerPreCheckoutQuery OK = TRUE]
K--> |Deny| G[Api::answerPreCheckoutQuery OK = FALSE]
F--> Bot::addUserToSubscribersList
G--> Bot::sendDenialErrorMessage
```


### Preview
<img src="photos/subscribe.jpg" alt="Subscribe" width="300"/>
<img src="photos/checkout.jpg" alt="checkout" width="300"/>
<img src="photos/card.jpg" alt="checkout" width="300"/>
<img src="photos/checkout2.jpg" alt="checkout" width="300"/>
<img src="photos/success.jpg" alt="checkout" width="300"/>



<!--
To
**Create a testing invoice using [@ShopBot](https://t.me/shopbot) for testing purposes:**
<br>
1. Open the Telegram mobile app and search @ShopBot
2. Send ShopBot a command /invoice
3. Follow instructions then click Send Invoice button
## Testing Invoice & Payments
Never use real money to test your Bot invoice and payments. Telegram has a testing bots
specifically made for this purpose [@ShopBot](https://t.me/shopbot) & [@TestStore](https://t.me/teststore), see [payments](https://core.telegram.org/bots/payments#introducing-payments-2-0)
-->
Binary file added examples/PaidSubscriptionBot/photos/card.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/PaidSubscriptionBot/photos/checkout.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/PaidSubscriptionBot/photos/success.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions examples/PaidSubscriptionBot/src/PaidSubscriptionBot.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#include "PaidSubscriptionBot.hpp"
#include <cpr/cpr.h>
#include <fstream>
#include <string>

using namespace tgbotxx;

PaidSubscriptionBot::PaidSubscriptionBot(const std::string &token, const std::string &paymentProviderToken)
: Bot(token), m_paymentProviderToken(paymentProviderToken) {}

void PaidSubscriptionBot::onStart() {
loadSubscribedUsersIds(); // if bot was stopped from a previous run, load back the subscribed user ids.

// Register my commands
Ptr<BotCommand> subscribeCmd(new BotCommand());
subscribeCmd->command = "/subscribe";
subscribeCmd->description = "Subscribe now to get Bot services for only 1$!";
api()->setMyCommands({subscribeCmd});

std::cout << "Bot " << api()->getMe()->firstName << " Started\n";
}

void PaidSubscriptionBot::onStop() {
saveSubscribedUsersIds(); // Don't lose subscribed user ids! (will be better to save to a database at runtime), for this example's simplicity, let's use a file for simplicity.

std::cout << "Bot " << api()->getMe()->firstName << " Stopped\n";
}

void PaidSubscriptionBot::onCommand(const Ptr<Message> &message) {
if (message->text == "/subscribe") {
const Ptr<User> &sender = message->from;
const Ptr<Chat> &chat = message->chat;
if (isUserAlreadySubscribed(sender->id)) {
api()->sendMessage(chat->id, "You are already subscribed!");
} else {
subscribeNewUser(chat->id, sender);
}
}
}


bool PaidSubscriptionBot::isUserAlreadySubscribed(UserId id) {
return std::any_of(m_subscribers.begin(), m_subscribers.end(), [id](const UserId &uid) {
return id == uid;
});
}

void PaidSubscriptionBot::subscribeNewUser(ChatId chatId, tgbotxx::Ptr<User> newUser) {
// Prepare invoice for new user:
const std::string title = "Bot Subscription";
const std::string description = "With only 1$ life time subscription, you will have access to all bot services!";
const std::string payload =
"subscribe:" + std::to_string(newUser->id); // we will get this payload back in onPreCheckoutQuery
const std::string currency = "USD";
std::vector<Ptr<LabeledPrice>> prices;
Ptr<LabeledPrice> subscriptionPrice = std::make_shared<LabeledPrice>();
subscriptionPrice->label = "Subscription Price";
subscriptionPrice->amount = 300; // = 3$ original price (Price of the product in the smallest units of the currency integer)
prices.push_back(subscriptionPrice);
Ptr<LabeledPrice> discountPrice = std::make_shared<LabeledPrice>();
discountPrice->label = "Discount";
discountPrice->amount = -200; // = -2$ discount (Price of the product in the smallest units of the currency integer)
prices.push_back(discountPrice);

// **Important** Note: if you are testing your bot invoice payment, don't pay using real money, use a testing card provided by Telegram explained in README.md
try {
// Send invoice to new user
api()->sendInvoice(chatId, title, description, payload, m_paymentProviderToken, currency, prices);
}
catch (const Exception &e) { // Handle issues accordingly ...
api()->sendMessage(chatId,
"Sorry, an error occurred while sending you an invoice. Please report this issue to the admins and try again later.");
std::cerr << __func__ << ": " << e.what() << std::endl;
}
}

void PaidSubscriptionBot::onPreCheckoutQuery(const Ptr<PreCheckoutQuery> &preCheckoutQuery) {
std::cout << __func__ << ": " << preCheckoutQuery->toJson().dump(2) << std::endl;
std::string payload = preCheckoutQuery->invoicePayload;
std::string action = StringUtils::split(payload, ':')[0];
UserId payerId = std::stoull(StringUtils::split(payload, ':')[1]);

std::cout << "payload: " << payload << std::endl;
std::cout << "action: " << action << std::endl;
std::cout << "payerId: " << payerId << std::endl;

if (action ==
"subscribe") { // because we set payload in invoice to "subscribe:USER_ID", you can customize it as you like.
// here do your logic whether to allow or deny this subscription, let's say we only allow 100 subscribers.
if (m_subscribers.size() < 100) {
// Approve payment so user can carry on the payment procedure with the payment provider
bool paymentApproved = api()->answerPreCheckoutQuery(preCheckoutQuery->id, true);
if (paymentApproved) {
std::cout << "Payment approved successfully" << std::endl;
// Note: payment is approved means user can now proceed to the payment phase, it doesn't mean that the user sent the money.
// We will receive a successful payment message if the payment is successful, see onPaymentSuccessful()
} else {
std::cerr << "Payment did not get approved" << std::endl;
}
} else {
// Deny payment
api()->answerPreCheckoutQuery(preCheckoutQuery->id, false, "Sorry, maximum subscribers reached!");
}
}
}

void PaidSubscriptionBot::saveSubscribedUsersIds() {
std::ofstream ofs{"subscribers.txt"};
for (const UserId &id: m_subscribers) {
ofs << id << std::endl;
}
ofs.close();

std::cout << "Saved " << m_subscribers.size() << " subscribers" << std::endl;
}

void PaidSubscriptionBot::loadSubscribedUsersIds() {
if (std::ifstream ifs{"subscribers.txt"}) {
std::string idstr;
while (std::getline(ifs, idstr)) {
m_subscribers.push_back(std::stoll(idstr));
}
ifs.close();
}
std::cout << "Loaded " << m_subscribers.size() << " subscribers" << std::endl;
}

void PaidSubscriptionBot::onLongPollError(const std::string &reason) {
std::cerr << __func__ << ": " << reason << std::endl;
}

void PaidSubscriptionBot::onAnyMessage(const Ptr<Message> &message) {
if (message->successfulPayment) {
// Handle successful payments messages
this->onSuccessfulPayment(message->successfulPayment);
}
}

void PaidSubscriptionBot::onSuccessfulPayment(const Ptr<SuccessfulPayment> &successfulPayment) {
std::string payload = successfulPayment->invoicePayload;
std::string action = StringUtils::split(payload, ':')[0];
UserId payerId = std::stoull(StringUtils::split(payload, ':')[1]);
if (action == "subscribe") {
m_subscribers.push_back(payerId); // or save subscriber UserId to a database...
std::cout << "New user subscription complete: userId: " << payerId << std::endl;
}
}
30 changes: 30 additions & 0 deletions examples/PaidSubscriptionBot/src/PaidSubscriptionBot.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once
#include <tgbotxx/tgbotxx.hpp>

class PaidSubscriptionBot : public tgbotxx::Bot {
using UserId = decltype(tgbotxx::User::id);
using ChatId = decltype(tgbotxx::Chat::id);

public:
PaidSubscriptionBot(const std::string &token, const std::string& paymentProviderToken);

private:
void onStart() override;
void onCommand(const tgbotxx::Ptr<tgbotxx::Message> &message) override;
void onAnyMessage(const tgbotxx::Ptr<tgbotxx::Message> &message) override;
void onStop() override;
/// Called when a new incoming pre-checkout query is received. Contains full information about checkout
void onPreCheckoutQuery(const tgbotxx::Ptr<tgbotxx::PreCheckoutQuery> &preCheckoutQuery) override;
void onLongPollError(const std::string &reason) override;

private:
bool isUserAlreadySubscribed(UserId id);
void subscribeNewUser(ChatId chatId, tgbotxx::Ptr<tgbotxx::User> newUser);
void onSuccessfulPayment(const tgbotxx::Ptr<tgbotxx::SuccessfulPayment>& successfulPayment);
void saveSubscribedUsersIds();
void loadSubscribedUsersIds();

private:
std::string m_paymentProviderToken; // tip: better get from ENV on production
std::vector<UserId> m_subscribers; // tip: better use a realtime database to store subscribers info
};
31 changes: 31 additions & 0 deletions examples/PaidSubscriptionBot/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include "PaidSubscriptionBot.hpp"
#include <csignal>

int main(int argc, const char *argv[]) {
if (argc < 3) {
std::cerr << "Usage:\npaid_subscription_bot \"BOT_TOKEN\" \"PAYMENT_PROVIDER_TOKEN\"\n";
return EXIT_FAILURE;
}

// Create a single instance of the bot
static std::unique_ptr<PaidSubscriptionBot> BOT(new PaidSubscriptionBot(argv[1], argv[2]));

// Graceful Bot exit on critical signals (CTRL+C, abort, seg fault...)
for(const int sig : {/*Ctrl+C*/SIGINT,
/*sys kill*/SIGKILL,
/*std::abort*/SIGABRT,
/*seg fault*/SIGSEGV,
/*std::terminate*/SIGTERM})
{
std::signal(sig, [](int s) {
if(BOT) {
BOT->stop();
}
std::exit(s == SIGINT ? EXIT_SUCCESS : EXIT_FAILURE);
});
}

// Start the bot
BOT->start();
return EXIT_SUCCESS;
}

0 comments on commit da20a7c

Please sign in to comment.