From 9e86f7a15bc26ee6a0443aeaf3fbea6742e1c236 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Mon, 27 Nov 2023 08:48:52 +0000 Subject: [PATCH 1/4] Expose the http handler to allow for use in user-defined servers --- ext/updater.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ext/updater.go b/ext/updater.go index 1f204274..ec2a4633 100644 --- a/ext/updater.go +++ b/ext/updater.go @@ -350,6 +350,12 @@ func (u *Updater) SetAllBotWebhooks(domain string, opts *gotgbot.SetWebhookOpts) return nil } +// GetHandler returns the http.Handler responsible for processing incoming webhook updates. +// It is provided to allow for an alternative to the StartServer method, using a user-defined http server. +func (u *Updater) GetHandler() http.Handler { + return &u.botMapping +} + // StartServer starts the webhook server for all the bots added via AddWebhook. // It is recommended to call this BEFORE calling setWebhooks. // The opts parameter allows for specifying TLS settings. @@ -370,7 +376,7 @@ func (u *Updater) StartServer(opts WebhookOpts) error { } u.webhookServer = &http.Server{ - Handler: &u.botMapping, + Handler: u.GetHandler(), ReadTimeout: opts.ReadTimeout, ReadHeaderTimeout: opts.ReadHeaderTimeout, } From ec944b0b3823de593f8d891fa15c8c510dbe9881 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Mon, 27 Nov 2023 19:17:42 +0000 Subject: [PATCH 2/4] Add an example which uses a custom-defined server, and test it --- ext/botmapping.go | 60 +++++++++++++++++++-------------------- ext/updater.go | 10 +++---- samples/webappBot/main.go | 30 +++++++++++++++++--- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/ext/botmapping.go b/ext/botmapping.go index 3a020b79..cf0d2316 100644 --- a/ext/botmapping.go +++ b/ext/botmapping.go @@ -127,42 +127,42 @@ func (m *botMapping) getBotFromURL(urlPath string) (botData, bool) { return bData, ok } -// ServeHTTP dispatches the request to the handler whose -// pattern most closely matches the request URL. -func (m *botMapping) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "*" { - if r.ProtoAtLeast(1, 1) { - w.Header().Set("Connection", "close") +func (m *botMapping) getHandlerFunc(prefix string) func(writer http.ResponseWriter, request *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "*" { + if r.ProtoAtLeast(1, 1) { + w.Header().Set("Connection", "close") + } + w.WriteHeader(http.StatusBadRequest) + return } - w.WriteHeader(http.StatusBadRequest) - return - } - b, ok := m.getBotFromURL(strings.TrimPrefix(r.URL.Path, "/")) - if !ok { - // If we don't recognise the URL, we return a 404. - w.WriteHeader(http.StatusNotFound) - return - } + b, ok := m.getBotFromURL(strings.TrimPrefix(r.URL.Path, prefix)) + if !ok { + // If we don't recognise the URL, we return a 404. + w.WriteHeader(http.StatusNotFound) + return + } - headerSecret := r.Header.Get("X-Telegram-Bot-Api-Secret-Token") - if b.webhookSecret != "" && b.webhookSecret != headerSecret { - // Drop any updates from invalid secret tokens. - w.WriteHeader(http.StatusUnauthorized) - return - } + headerSecret := r.Header.Get("X-Telegram-Bot-Api-Secret-Token") + if b.webhookSecret != "" && b.webhookSecret != headerSecret { + // Drop any updates from invalid secret tokens. + w.WriteHeader(http.StatusUnauthorized) + return + } - bytes, err := io.ReadAll(r.Body) - if err != nil { - if m.errFunc != nil { - m.errFunc(err) - } else { - m.logf("Failed to read incoming update contents: %s", err.Error()) + bytes, err := io.ReadAll(r.Body) + if err != nil { + if m.errFunc != nil { + m.errFunc(err) + } else { + m.logf("Failed to read incoming update contents: %s", err.Error()) + } + w.WriteHeader(http.StatusInternalServerError) + return } - w.WriteHeader(http.StatusInternalServerError) - return + b.updateChan <- bytes } - b.updateChan <- bytes } func (m *botMapping) logf(format string, args ...interface{}) { diff --git a/ext/updater.go b/ext/updater.go index ec2a4633..45c1cdd5 100644 --- a/ext/updater.go +++ b/ext/updater.go @@ -350,10 +350,10 @@ func (u *Updater) SetAllBotWebhooks(domain string, opts *gotgbot.SetWebhookOpts) return nil } -// GetHandler returns the http.Handler responsible for processing incoming webhook updates. -// It is provided to allow for an alternative to the StartServer method, using a user-defined http server. -func (u *Updater) GetHandler() http.Handler { - return &u.botMapping +// GetHandlerFunc returns the http.HandlerFunc responsible for processing incoming webhook updates. +// It is provided to allow for an alternative to the StartServer method using a user-defined http server. +func (u *Updater) GetHandlerFunc(pathPrefix string) http.HandlerFunc { + return u.botMapping.getHandlerFunc(pathPrefix) } // StartServer starts the webhook server for all the bots added via AddWebhook. @@ -376,7 +376,7 @@ func (u *Updater) StartServer(opts WebhookOpts) error { } u.webhookServer = &http.Server{ - Handler: u.GetHandler(), + Handler: u.GetHandlerFunc("/"), ReadTimeout: opts.ReadTimeout, ReadHeaderTimeout: opts.ReadHeaderTimeout, } diff --git a/samples/webappBot/main.go b/samples/webappBot/main.go index 26ad7cd1..621af081 100644 --- a/samples/webappBot/main.go +++ b/samples/webappBot/main.go @@ -33,6 +33,12 @@ func main() { panic("URL environment variable is empty") } + // Get the webhook secret from the environment variable. + webhookSecret := os.Getenv("WEBHOOK_SECRET") + if webhookSecret == "" { + panic("WEBHOOK_SECRET environment variable is empty") + } + // Create our bot. b, err := gotgbot.NewBot(token, nil) if err != nil { @@ -52,15 +58,27 @@ func main() { // /start command to introduce the bot and send the URL dispatcher.AddHandler(handlers.NewCommand("start", func(b *gotgbot.Bot, ctx *ext.Context) error { + // We can wrap commands with anonymous functions to pass in extra variables, like the webapp URL, or other + // configuration. return start(b, ctx, webappURL) })) - // Start receiving (and handling) updates. - err = updater.StartPolling(b, &ext.PollingOpts{DropPendingUpdates: true}) + // We add the bot webhook to our updater, such that we can populate the updater's http.Handler. + err = updater.AddWebhook(b, b.Token, ext.WebhookOpts{SecretToken: webhookSecret}) if err != nil { - panic("failed to start polling: " + err.Error()) + panic("Failed to add bot webhooks to updater: " + err.Error()) + } + + // We select a subpath to specify where the updater handler is found on the http.Server. + updaterSubpath := "/bots/" + err = updater.SetAllBotWebhooks(webappURL+updaterSubpath, &gotgbot.SetWebhookOpts{ + MaxConnections: 100, + DropPendingUpdates: true, + SecretToken: webhookSecret, + }) + if err != nil { + panic("Failed to set bot webhooks: " + err.Error()) } - log.Printf("%s has been started...\n", b.User.Username) // Setup new HTTP server mux to handle different paths. mux := http.NewServeMux() @@ -68,11 +86,15 @@ func main() { mux.HandleFunc("/", index(webappURL)) // This serves our "validation" API, which checks if the input data is valid. mux.HandleFunc("/validate", validate(token)) + // This serves the updater's webhook handler. + mux.HandleFunc(updaterSubpath, updater.GetHandlerFunc(updaterSubpath)) + server := http.Server{ Handler: mux, Addr: "0.0.0.0:8080", } + log.Printf("%s has been started...\n", b.User.Username) // Start the webserver displaying the page. // Note: ListenAndServe is a blocking operation, so we don't need to call updater.Idle() here. if err := server.ListenAndServe(); err != nil { From e48df68a1252e24d36edd4c708364c659fc3bbb1 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Tue, 28 Nov 2023 08:44:25 +0000 Subject: [PATCH 3/4] comments --- samples/webappBot/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/samples/webappBot/main.go b/samples/webappBot/main.go index 621af081..d03046b0 100644 --- a/samples/webappBot/main.go +++ b/samples/webappBot/main.go @@ -20,6 +20,8 @@ import ( // from the samples/webappBot directory: // `URL="" TOKEN="" go run .` // Then, simply send /start to your bot, and enjoy your webapp demo. +// +// This example also demonstrates how to use the updater's handler in a user-provided server. func main() { // Get token from the environment variable token := os.Getenv("TOKEN") From b5ab2dd73045de26ce2bca2fd7e91bfb19b1cca5 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Tue, 28 Nov 2023 08:46:41 +0000 Subject: [PATCH 4/4] Update comments --- samples/README.md | 2 ++ scripts/ci/generate-sample-bot-descriptions.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/README.md b/samples/README.md index 81ac72b8..c848fa46 100644 --- a/samples/README.md +++ b/samples/README.md @@ -77,3 +77,5 @@ Then, copy-paste the HTTPS URL obtained from ngrok (changes every time you run i from the samples/webappBot directory: `URL="" TOKEN="" go run .` Then, simply send /start to your bot, and enjoy your webapp demo. + +This example also demonstrates how to use the updater's handler in a user-provided server. diff --git a/scripts/ci/generate-sample-bot-descriptions.sh b/scripts/ci/generate-sample-bot-descriptions.sh index 106923da..d114ae06 100755 --- a/scripts/ci/generate-sample-bot-descriptions.sh +++ b/scripts/ci/generate-sample-bot-descriptions.sh @@ -29,7 +29,7 @@ for d in "${SAMPLES_DIR}"/*; do echo "$intro" >>"${README_FILE}" # Extract all comments before the main function, and dump them in the readme file. - description="$(sed -n -e '/package/,/func main/ p' "${d}/main.go" | (grep -E "^//" || true) | sed -e 's:// ::g')" + description="$(sed -n -e '/package/,/func main/ p' "${d}/main.go" | (grep -E "^//" || true) | sed -E 's:^// ?::g')" if [[ -z "${description}" ]]; then echo "!!! no doc comments for ${d} - please add some first." exit 1