Skip to content

Commit

Permalink
Added sendInvoice payments api method
Browse files Browse the repository at this point in the history
  • Loading branch information
baderouaich committed Dec 27, 2023
1 parent 3ae242b commit 2a06f86
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 13 deletions.
69 changes: 69 additions & 0 deletions include/tgbotxx/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ namespace tgbotxx {
struct ChatAdministratorRights;
struct WebhookInfo;
struct Poll;
struct LabeledPrice;

/// @brief Api Methods https://core.telegram.org/bots/api#available-methods
/// @note All methods in the Bot API are case-insensitive.
Expand Down Expand Up @@ -1382,6 +1383,74 @@ namespace tgbotxx {
Ptr<ChatAdministratorRights> getMyDefaultAdministratorRights(bool forChannels = false) const;


public: /// Payments methods https://core.telegram.org/bots/api#payments
/// @brief Use this method to send invoices.
/// @param chatId Integer or String Unique identifier for the target chat or username of the target channel (in the format @channelusername)
/// @param title Product name, 1-32 characters
/// @param description Product description, 1-255 characters
/// @param payload Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes.
/// @param providerToken Payment provider token, obtained via @BotFather
/// @param currency Three-letter ISO 4217 currency code, [see more on currencies](https://core.telegram.org/bots/payments#supported-currencies)
/// @param prices Array of LabeledPrice, Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
/// @param messageThreadId Optional. Unique identifier for the target message thread (topic) of the forum; for forum supergroups only
/// @param maxTipAmount Optional. The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double).
/// For example, for a maximum tip of US$ 1.45 pass max_tip_amount = 145. See the exp parameter in currencies.json,
/// it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0
/// @param suggestedTipAmounts Optional. A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, not float/double).
/// At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed maxTipAmount.
/// @param startParameter Optional. Unique deep-linking parameter. If left empty, forwarded copies of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message,
/// using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter
/// @param providerData Optional. JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.
/// @param photoUrl Optional. URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.
/// @param photoSize Optional. Photo size in bytes
/// @param photoWidth Optional. Photo width
/// @param photoHeight Optional. Photo height
/// @param needName Optional. Pass True if you require the user's full name to complete the order
/// @param needPhoneNumber Optional. Pass True if you require the user's phone number to complete the order
/// @param needEmail Optional. Pass True if you require the user's email address to complete the order
/// @param needShippingAddress Optional. Pass True if you require the user's shipping address to complete the order
/// @param sendPhoneNumberToProvider Optional. Pass True if the user's phone number should be sent to provider
/// @param sendEmailToProvider Optional. Pass True if the user's email address should be sent to provider
/// @param isFlexible Optional. Pass True if the final price depends on the shipping method
/// @param disableNotification Optional. Sends the message silently. Users will receive a notification with no sound.
/// @param protectContent Optional. Protects the contents of the sent message from forwarding and saving
/// @param replyToMessageId Optional. If the message is a reply, ID of the original message
/// @param allowSendingWithoutReply Optional. Pass True if the message should be sent even if the specified replied-to message is not found
/// @param replyMarkup Optional. A JSON-serialized object for an inline keyboard. If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button.
/// One of InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or ForceReply.
/// @returns sent Message object on success.
/// @throws Exception on failure
/// @ref https://core.telegram.org/bots/api#sendinvoice
Ptr<Message> sendInvoice(const std::variant<std::int64_t, std::string>& chatId,
const std::string& title,
const std::string& description,
const std::string& payload,
const std::string& providerToken,
const std::string& currency,
const std::vector<Ptr<LabeledPrice>>& prices,
std::int32_t messageThreadId = 0,
std::int32_t maxTipAmount = 0,
const std::vector<std::int32_t>& suggestedTipAmounts = std::vector<std::int32_t>(),
const std::string& startParameter = "",
const std::string& providerData = "",
const std::string& photoUrl = "",
std::int32_t photoSize = 0,
std::int32_t photoWidth = 0,
std::int32_t photoHeight = 0,
bool needName = false,
bool needPhoneNumber = false,
bool needEmail = false,
bool needShippingAddress = false,
bool sendPhoneNumberToProvider = false,
bool sendEmailToProvider = false,
bool isFlexible = false,
bool disableNotification = false,
bool protectContent = false,
std::int32_t replyToMessageId = 0,
bool allowSendingWithoutReply = false,
const Ptr<IReplyMarkup>& replyMarkup = nullptr) const;


public: /// Updates methods https://core.telegram.org/bots/api#getting-updates
/// @brief Use this method to receive incoming updates using long polling.
/// @param offset Identifier of the first update to be returned. Must be greater by one than the highest
Expand Down
106 changes: 105 additions & 1 deletion src/Api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2072,4 +2072,108 @@ cpr::Timeout Api::getUploadFilesTimeout() const noexcept { return m_uploadFilesT
void Api::setDownloadFilesTimeout(const cpr::Timeout& timeout) noexcept {
m_downloadFilesTimeout = timeout;
}
cpr::Timeout Api::getDownloadFilesTimeout() const noexcept { return m_downloadFilesTimeout; }
cpr::Timeout Api::getDownloadFilesTimeout() const noexcept { return m_downloadFilesTimeout; }


/////////////////////////////////////////////////////////////////////////////////////////////////
Ptr<Message> Api::sendInvoice(const std::variant<std::int64_t, std::string>& chatId,
const std::string& title,
const std::string& description,
const std::string& payload,
const std::string& providerToken,
const std::string& currency,
const std::vector<Ptr<LabeledPrice>>& prices,
std::int32_t messageThreadId,
std::int32_t maxTipAmount,
const std::vector<std::int32_t>& suggestedTipAmounts,
const std::string& startParameter,
const std::string& providerData,
const std::string& photoUrl,
std::int32_t photoSize,
std::int32_t photoWidth,
std::int32_t photoHeight,
bool needName,
bool needPhoneNumber,
bool needEmail,
bool needShippingAddress,
bool sendPhoneNumberToProvider,
bool sendEmailToProvider,
bool isFlexible,
bool disableNotification,
bool protectContent,
std::int32_t replyToMessageId,
bool allowSendingWithoutReply,
const Ptr<IReplyMarkup>& replyMarkup) const {

cpr::Multipart data{};
data.parts.reserve(28);
switch (chatId.index()) {
case 0: // std::int64_t
if (std::int64_t chatIdInt = std::get<std::int64_t>(chatId); chatIdInt != 0) {
data.parts.emplace_back("chat_id", std::to_string(chatIdInt));
}
break;
case 1: // std::string
if (std::string chatIdStr = std::get<std::string>(chatId); not chatIdStr.empty()) {
data.parts.emplace_back("chat_id", chatIdStr);
}
break;
default:
break;
}
data.parts.emplace_back("title", title);
data.parts.emplace_back("description", description);
data.parts.emplace_back("payload", payload);
data.parts.emplace_back("provider_token", providerToken);
data.parts.emplace_back("currency", currency);
nl::json pricesJson = nl::json::array();
for (const Ptr<LabeledPrice>& price: prices)
pricesJson.push_back(price->toJson());
data.parts.emplace_back("prices", pricesJson.dump());
if (messageThreadId)
data.parts.emplace_back("message_thread_id", messageThreadId);
if (maxTipAmount)
data.parts.emplace_back("max_tip_amount", maxTipAmount);
if (not suggestedTipAmounts.empty())
data.parts.emplace_back("suggested_tip_amounts", nl::json(suggestedTipAmounts).dump());
if (not startParameter.empty())
data.parts.emplace_back("start_parameter", startParameter);
if (not providerData.empty())
data.parts.emplace_back("provider_data", providerData);
if (not photoUrl.empty())
data.parts.emplace_back("photo_url", photoUrl);
if (photoSize)
data.parts.emplace_back("photo_size", photoSize);
if (photoWidth)
data.parts.emplace_back("photo_width", photoWidth);
if (photoHeight)
data.parts.emplace_back("photo_height", photoHeight);
if (needName)
data.parts.emplace_back("need_name", needName);
if (needPhoneNumber)
data.parts.emplace_back("need_phone_number", needPhoneNumber);
if (needEmail)
data.parts.emplace_back("need_email", needEmail);
if (needShippingAddress)
data.parts.emplace_back("need_shipping_address", needShippingAddress);
if (sendPhoneNumberToProvider)
data.parts.emplace_back("send_phone_number_to_provider", sendPhoneNumberToProvider);
if (sendEmailToProvider)
data.parts.emplace_back("send_email_to_provider", sendEmailToProvider);
if (isFlexible)
data.parts.emplace_back("is_flexible", isFlexible);
if (disableNotification)
data.parts.emplace_back("disable_notification", disableNotification);
if (protectContent)
data.parts.emplace_back("protect_content", protectContent);
if (replyToMessageId)
data.parts.emplace_back("reply_to_message_id", replyToMessageId);
if (allowSendingWithoutReply)
data.parts.emplace_back("allow_sending_without_reply", allowSendingWithoutReply);
if (replyMarkup)
data.parts.emplace_back("reply_markup", replyMarkup->toJson().dump());

nl::json sentMessageObj = sendRequest("sendInvoice", data);
Ptr<Message> message(new Message(sentMessageObj));
return message;
}
50 changes: 38 additions & 12 deletions tests/manual_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ 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));
// 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");
// api()->setMyName("tgbotxx manual_tests");
// api()->setMyDescription("tgbotxx bot manual tests");

// Register bot commands ...
Ptr<BotCommand> greet(new BotCommand());
Expand Down Expand Up @@ -91,7 +91,13 @@ class MyBot : public Bot {
Ptr<BotCommand> deleteMessage(new BotCommand());
deleteMessage->command = "/delete_message";
deleteMessage->description = "You will receive a message then it will be deleted after 2 seconds.";
getApi()->setMyCommands({greet, stop, photo, buttons, audio, document, animation, voice, mediaGroup, location, userProfilePhotos, ban, poll, quiz, webhookInfo, botName, menuButtonWebApp, menuButtonDefault, showAdministratorRights, editMessageText, deleteMessage}); // The above commands will be shown in the bot chat menu (bottom left)
Ptr<BotCommand> sendInvoice(new BotCommand());
sendInvoice->command = "/send_invoice";
sendInvoice->description = "You will receive a test invoice";
getApi()->setMyCommands({greet, stop, photo, buttons, audio, document, animation, voice, mediaGroup,
location, userProfilePhotos, ban, poll, quiz, webhookInfo, botName,
menuButtonWebApp, menuButtonDefault, showAdministratorRights, editMessageText,
deleteMessage, sendInvoice}); // The above commands will be shown in the bot chat menu (bottom left)

std::cout << __func__ << ": " << api()->getMyName()->name << " bot started!" << std::endl;
}
Expand Down Expand Up @@ -123,7 +129,7 @@ class MyBot : public Bot {
}

void onLongPollError(const std::string& reason) override {
std::cerr << "Long polling error: " << reason << std::endl;
std::cerr << "Long polling error: " << reason << std::endl;
}

/// Called when a new command is received (messages with leading '/' char).
Expand Down Expand Up @@ -271,17 +277,29 @@ class MyBot : public Bot {
api()->sendMessage(message->chat->id, chatAdministratorRights->toJson().dump(2));
} else if (message->text == "/edit_message_text") {
Ptr<Message> originalMessage = api()->sendMessage(message->chat->id, "Progress started...");
for(int i = 0; i <= 100; i += 10) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::ostringstream oss{};
oss << "Progress: " << i << '/' << 100;
api()->editMessageText(oss.str(), originalMessage->chat->id, originalMessage->messageId);
for (int i = 0; i <= 100; i += 10) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::ostringstream oss{};
oss << "Progress: " << i << '/' << 100;
api()->editMessageText(oss.str(), originalMessage->chat->id, originalMessage->messageId);
}
api()->editMessageText("Done.", originalMessage->chat->id, originalMessage->messageId);
} else if (message->text == "/delete_message") {
Ptr<Message> twoSecondsMsg = api()->sendMessage(message->chat->id, "Hello! my life span is 2 seconds only so I don't have much time. Goodbye!");
std::this_thread::sleep_for(std::chrono::seconds(2));
api()->deleteMessage(twoSecondsMsg->chat->id, twoSecondsMsg->messageId);
} else if (message->text == "/send_invoice") {
const std::string providerToken = getPaymentProviderToken();
std::vector<Ptr<LabeledPrice>> prices;
Ptr<LabeledPrice> originalPrice = std::make_shared<LabeledPrice>();
originalPrice->label = "Original price";
originalPrice->amount = 150; // 1.50$
prices.push_back(originalPrice);
Ptr<LabeledPrice> discountPrice = std::make_shared<LabeledPrice>();
discountPrice->label = "Discount";
discountPrice->amount = -50; // -0.5$
prices.push_back(discountPrice);
api()->sendInvoice(message->chat->id, "Product name", "Product description", "payload", providerToken, "USD", prices);
}
}

Expand Down Expand Up @@ -342,8 +360,16 @@ class MyBot : public Bot {
void onChatJoinRequest(const Ptr<ChatJoinRequest>& chatJoinRequest) override {
std::cout << __func__ << ": " << chatJoinRequest->from->username << std::endl;
}

private:
static std::string getPaymentProviderToken(){
if (char *token = std::getenv("TESTS_PAYMENT_PROVIDER_TOKEN"); token != nullptr)
return std::string(token);
throw std::runtime_error("Couldn't find PAYMENT_PROVIDER_TOKEN in the env; please export an environment variable PAYMENT_PROVIDER_TOKEN with your payment provider token");
}
};


static std::string getToken() {
if (char *token = std::getenv("TESTS_BOT_TOKEN"); token != nullptr)
return std::string(token);
Expand All @@ -359,7 +385,7 @@ int main() try {
});
bot.start();
return EXIT_SUCCESS;
} catch (const std::exception& e){
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}

0 comments on commit 2a06f86

Please sign in to comment.