-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5c5abb0
commit da20a7c
Showing
10 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
--> |
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.
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.
147 changes: 147 additions & 0 deletions
147
examples/PaidSubscriptionBot/src/PaidSubscriptionBot.cpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |