Skip to content

Commit

Permalink
Payments bot sample (#175)
Browse files Browse the repository at this point in the history
* set up payments bot sample

* Regen docs + add paysupport command

* lint
  • Loading branch information
PaulSonOfLars authored Aug 18, 2024
1 parent 10c3bf6 commit e0be7dd
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 2 deletions.
7 changes: 7 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ could be collected.
This bot shows how to effectively use middlewares to modify and intercept HTTP requests to the bot API server.
In this example, the middleware sets the allow_sending_without_reply to certain methods, as well as make sure to log all error messages.

## samples/paymentsBot

This bot demonstrates how to provide invoices, checkouts, and successful payments through telegram's in-app purchase
methods.
Use this if you want an example of how to sell things through telegram. The example targets Telegram Stars, which
allows bot developers to sell digital products through Telegram.

## samples/statefulClientBot

This bot demonstrates how to pass around variables to all handlers without changing any function signatures.
Expand Down
4 changes: 2 additions & 2 deletions samples/commandBot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ func source(b *gotgbot.Bot, ctx *ext.Context) error {

// start introduces the bot.
func start(b *gotgbot.Bot, ctx *ext.Context) error {
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("Hello, I'm @%s. I <b>repeat</b> all your messages.", b.User.Username), &gotgbot.SendMessageOpts{
ParseMode: "html",
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("Hello, I'm @%s.\nI am a sample bot to demonstrate how file sending works.\n\nTry the /source command!", b.User.Username), &gotgbot.SendMessageOpts{
ParseMode: "HTML",
})
if err != nil {
return fmt.Errorf("failed to send start message: %w", err)
Expand Down
10 changes: 10 additions & 0 deletions samples/paymentsBot/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/PaulSonOfLars/gotgbot/samples/paymentsBot

go 1.19

require (
github.com/PaulSonOfLars/gotgbot/v2 v2.99.99
github.com/google/uuid v1.6.0
)

replace github.com/PaulSonOfLars/gotgbot/v2 => ../../
2 changes: 2 additions & 0 deletions samples/paymentsBot/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140 changes: 140 additions & 0 deletions samples/paymentsBot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package main

import (
"fmt"
"log"
"os"
"time"

"github.com/google/uuid"

"github.com/PaulSonOfLars/gotgbot/v2"
"github.com/PaulSonOfLars/gotgbot/v2/ext"
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/message"
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/precheckoutquery"
)

// This bot demonstrates how to provide invoices, checkouts, and successful payments through telegram's in-app purchase
// methods.
// Use this if you want an example of how to sell things through telegram. The example targets Telegram Stars, which
// allows bot developers to sell digital products through Telegram.
func main() {
// Get token from the environment variable
token := os.Getenv("TOKEN")
if token == "" {
panic("TOKEN environment variable is empty")
}

// Create bot from environment value.
b, err := gotgbot.NewBot(token, nil)
if err != nil {
panic("failed to create new bot: " + err.Error())
}

// Create updater and dispatcher.
dispatcher := ext.NewDispatcher(&ext.DispatcherOpts{
// If an error is returned by a handler, log it and continue going.
Error: func(b *gotgbot.Bot, ctx *ext.Context, err error) ext.DispatcherAction {
log.Println("an error occurred while handling update:", err.Error())
return ext.DispatcherActionNoop
},
MaxRoutines: ext.DefaultMaxRoutines,
})
updater := ext.NewUpdater(dispatcher, nil)

// /start command to introduce the bot
dispatcher.AddHandler(handlers.NewCommand("start", start))
// PreCheckout to handle the step right before payment. Must be handled within 10s, or the checkout will be abandoned by telegram.
dispatcher.AddHandler(handlers.NewPreCheckoutQuery(precheckoutquery.All, preCheckout))
// Payment received; send/provide product to customer.
dispatcher.AddHandler(handlers.NewMessage(message.SuccessfulPayment, paymentComplete))
// Bots selling on telegram must be able to provide refunds; do so through the paysupport command, as mentioned in
// the TOS: https://telegram.org/tos/stars#3-1-disputing-purchases
dispatcher.AddHandler(handlers.NewCommand("paysupport", paySupport))

// Start receiving updates.
err = updater.StartPolling(b, &ext.PollingOpts{
DropPendingUpdates: true,
GetUpdatesOpts: &gotgbot.GetUpdatesOpts{
Timeout: 9,
RequestOpts: &gotgbot.RequestOpts{
Timeout: time.Second * 10,
},
},
})
if err != nil {
panic("failed to start polling: " + err.Error())
}
log.Printf("%s has been started...\n", b.User.Username)

// Idle, to keep updates coming in, and avoid bot stopping.
updater.Idle()
}

// start introduces the bot and sends an initial invoice (in Telegram stars; denoted as XTR).
func start(b *gotgbot.Bot, ctx *ext.Context) error {
if ctx.EffectiveChat.Type != "private" {
// Only reply in private chats.
return nil
}

// Introduce the bot.
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("Hello, I'm @%s. I demonstrate how telegram payments might work.", b.User.Username), &gotgbot.SendMessageOpts{
ParseMode: "HTML",
})
if err != nil {
return fmt.Errorf("failed to send start message: %w", err)
}

// Generate a unique payload for this checkout.
// In a production environment, you could create a database containing any additional information you might need to
// complete this transaction, and refer to it at the preCheckout and successful payment stages.
// For example, you may want to store the invoice creator's ID to ensure that only the creator can pay, and any
// other necessary data you have collected.
payload := uuid.NewString()

// Send the invoice. XTR == Telegram stars, for selling digital products.
_, err = b.SendInvoice(ctx.EffectiveChat.Id, "Product Name", "Some detailed description", payload, "XTR", []gotgbot.LabeledPrice{{
Label: "Some product",
Amount: 100, // 100 stars.
}}, &gotgbot.SendInvoiceOpts{
ProtectContent: true, // Stop people from forwarding this invoice to others.
})
if err != nil {
return fmt.Errorf("failed to generate invoice: %w", err)
}

return nil
}

func preCheckout(b *gotgbot.Bot, ctx *ext.Context) error {
// Do any required preCheckout validation here. If anything failed, we should answer the query with "ok: False",
// and populate the ErrorMessage field in the opts.
// For example, you may want to ensure that the user who requested the invoice is the same person as the person who
// is checking out; but this would require storage, so isn't shown here.

// Answer true once checks have passed.
_, err := ctx.PreCheckoutQuery.Answer(b, true, nil)
if err != nil {
return fmt.Errorf("failed to answer precheckout query: %w", err)
}
return nil
}

func paymentComplete(b *gotgbot.Bot, ctx *ext.Context) error {
// Payment has been received; a real bot would now provide the user with the product.
_, err := ctx.EffectiveMessage.Reply(b, "Payment complete - in a real bot, this is where you would provision the product that has been paid for.", nil)
if err != nil {
return fmt.Errorf("failed to send payment complete message: %w", err)
}
return nil
}

func paySupport(b *gotgbot.Bot, ctx *ext.Context) error {
_, err := ctx.EffectiveMessage.Reply(b, "Explain your refund process here.", nil)
if err != nil {
return fmt.Errorf("failed to describe refund process: %w", err)
}
return nil
}

0 comments on commit e0be7dd

Please sign in to comment.