From 73517fa04c1d90451c4b4c4c5f7d66b27a7587b8 Mon Sep 17 00:00:00 2001 From: Paul Larsen Date: Sat, 2 Dec 2023 15:07:08 +0000 Subject: [PATCH] Expose http handler (#124) * Expose the http handler to allow for use in user-defined servers * Add an example which uses a custom-defined server, and test it * comments * Update comments --- ext/botmapping.go | 60 +++++++++---------- ext/updater.go | 8 ++- samples/README.md | 2 + samples/webappBot/main.go | 32 ++++++++-- .../ci/generate-sample-bot-descriptions.sh | 2 +- 5 files changed, 68 insertions(+), 36 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 1f204274..45c1cdd5 100644 --- a/ext/updater.go +++ b/ext/updater.go @@ -350,6 +350,12 @@ func (u *Updater) SetAllBotWebhooks(domain string, opts *gotgbot.SetWebhookOpts) return nil } +// 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. // 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.GetHandlerFunc("/"), ReadTimeout: opts.ReadTimeout, ReadHeaderTimeout: opts.ReadHeaderTimeout, } 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/samples/webappBot/main.go b/samples/webappBot/main.go index 26ad7cd1..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") @@ -33,6 +35,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 +60,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 +88,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 { 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