diff --git a/samples/README.md b/samples/README.md index c848fa4..f2cfbf5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -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. diff --git a/samples/commandBot/main.go b/samples/commandBot/main.go index 0062ad9..0f284fd 100644 --- a/samples/commandBot/main.go +++ b/samples/commandBot/main.go @@ -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 repeat 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) diff --git a/samples/paymentsBot/go.mod b/samples/paymentsBot/go.mod new file mode 100644 index 0000000..55d489d --- /dev/null +++ b/samples/paymentsBot/go.mod @@ -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 => ../../ diff --git a/samples/paymentsBot/go.sum b/samples/paymentsBot/go.sum new file mode 100755 index 0000000..7790d7c --- /dev/null +++ b/samples/paymentsBot/go.sum @@ -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= diff --git a/samples/paymentsBot/main.go b/samples/paymentsBot/main.go new file mode 100644 index 0000000..cd79b79 --- /dev/null +++ b/samples/paymentsBot/main.go @@ -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 +}