Skip to content

Commit

Permalink
Expose http handler (#124)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
PaulSonOfLars authored Dec 2, 2023
1 parent 9cb7cec commit 73517fa
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 36 deletions.
60 changes: 30 additions & 30 deletions ext/botmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {
Expand Down
8 changes: 7 additions & 1 deletion ext/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
}
Expand Down
2 changes: 2 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="<your_url_here>" TOKEN="<your_token_here>" 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.
32 changes: 28 additions & 4 deletions samples/webappBot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
// from the samples/webappBot directory:
// `URL="<your_url_here>" TOKEN="<your_token_here>" 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")
Expand All @@ -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 {
Expand All @@ -52,27 +60,43 @@ 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()
// This serves the home page.
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 {
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/generate-sample-bot-descriptions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 73517fa

Please sign in to comment.