From 1ef07c39bfbe5b16a93ad6beca2d03b15c396d5e Mon Sep 17 00:00:00 2001 From: Ilja Lapkovskis Date: Tue, 30 Jan 2024 21:46:49 +0200 Subject: [PATCH] Decouple bot handlers. --- bot.go | 142 +++++---------------------------------- bot_handler.go | 149 +++++++++++++++++++++++++++++++++++++++++ example/example_bot.go | 25 ++++--- 3 files changed, 181 insertions(+), 135 deletions(-) create mode 100644 bot_handler.go diff --git a/bot.go b/bot.go index da65260..7a3479d 100644 --- a/bot.go +++ b/bot.go @@ -11,7 +11,6 @@ import ( "net/http" "net/url" "strings" - "time" ) const DefaultBufferSize = 100 @@ -26,9 +25,6 @@ type BotAPI struct { config BotConfigI client HTTPClientI readonly bool - - buffer int - shutdownChannel chan interface{} } // NewBot creates a new BotAPI instance. @@ -44,10 +40,8 @@ func NewBot(config BotConfigI) *BotAPI { // It requires a token, provided by @BotFather on Telegram and API endpoint. func NewBotWithClient(config BotConfigI, client HTTPClientI) *BotAPI { bot := &BotAPI{ - config: config.(*BotConfig), - client: client, - buffer: DefaultBufferSize, - shutdownChannel: make(chan interface{}), + config: config.(*BotConfig), + client: client, } return bot @@ -383,6 +377,20 @@ func (bot *BotAPI) GetFile(config FileConfig) (File, error) { return file, err } +// GetWebhookInfo allows you to fetch information about a webhook and if +// one currently is set, along with pending update count and error messages. +func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) { + resp, err := bot.MakeRequest("getWebhookInfo", nil) + if err != nil { + return WebhookInfo{}, err + } + + var info WebhookInfo + err = json.Unmarshal(resp.Result, &info) + + return info, err +} + // GetUpdates fetches updates. // If a WebHook is set, this will not return any data! // @@ -402,124 +410,6 @@ func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { return updates, err } -// GetWebhookInfo allows you to fetch information about a webhook and if -// one currently is set, along with pending update count and error messages. -func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) { - resp, err := bot.MakeRequest("getWebhookInfo", nil) - if err != nil { - return WebhookInfo{}, err - } - - var info WebhookInfo - err = json.Unmarshal(resp.Result, &info) - - return info, err -} - -// GetUpdatesChan starts and returns a channel for getting updates. -func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { - ch := make(chan Update, bot.buffer) - - go func() { - for { - select { - case <-bot.shutdownChannel: - close(ch) - return - default: - } - - updates, err := bot.GetUpdates(config) - if err != nil { - log.Println(err) - log.Println("Failed to get updates, retrying in 3 seconds...") - time.Sleep(time.Second * 3) - - continue - } - - for _, update := range updates { - if update.UpdateID >= config.Offset { - config.Offset = update.UpdateID + 1 - ch <- update - } - } - } - }() - - return ch -} - -// StopReceivingUpdates stops the go routine which receives updates -func (bot *BotAPI) StopReceivingUpdates() { - if bot.config.GetDebug() { - log.Println("Stopping the update receiver routine...") - } - close(bot.shutdownChannel) -} - -// TODO: get rid of bot dependancy -// ListenForWebhook registers a http handler for a webhook. -func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { - ch := make(chan Update, bot.buffer) - - http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { - update, err := bot.HandleUpdate(r) - if err != nil { - errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(errMsg) - return - } - - ch <- *update - }) - - return ch -} - -// TODO: Remove dependancy on bot -// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook. -func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel { - ch := make(chan Update, bot.buffer) - - func(w http.ResponseWriter, r *http.Request) { - defer close(ch) - - update, err := bot.HandleUpdate(r) - if err != nil { - errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(errMsg) - return - } - - ch <- *update - }(w, r) - - return ch -} - -// TODO: move it outside of bot struct, it's not related to bot - -// HandleUpdate parses and returns update received via webhook -func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) { - if r.Method != http.MethodPost { - err := errors.New("wrong HTTP method required POST") - return nil, err - } - - var update Update - err := json.NewDecoder(r.Body).Decode(&update) - if err != nil { - return nil, err - } - - return &update, nil -} - // WriteToHTTPResponse writes the request to the HTTP ResponseWriter. // // It doesn't support uploading files. diff --git a/bot_handler.go b/bot_handler.go new file mode 100644 index 0000000..41ab73f --- /dev/null +++ b/bot_handler.go @@ -0,0 +1,149 @@ +package tgbotapi + +import ( + "encoding/json" + "errors" + "net/http" + "time" +) + +type HandlerConfig struct { + shutdownChannel chan struct{} + bufferSize int +} + +var defaultConfig = HandlerConfig{ + shutdownChannel: make(chan struct{}), + bufferSize: 100, +} + +type PollingHandler struct { + bot *BotAPI + HandlerConfig + updateConfig UpdateConfig +} + +func NewPollingHandler(bot *BotAPI, updateConfig UpdateConfig) *PollingHandler { + return &PollingHandler{ + bot: bot, + HandlerConfig: defaultConfig, + updateConfig: updateConfig, + } +} + +type WebhookHandler struct { + bot *BotAPI + HandlerConfig +} + +func NewWebhookHandler(bot *BotAPI) *WebhookHandler { + return &WebhookHandler{ + bot: bot, + HandlerConfig: defaultConfig, + } +} + +// InitUpdatesChannel starts and returns a channel for getting updates. +func (h *PollingHandler) InitUpdatesChannel() (UpdatesChannel, error) { + w, err := h.bot.GetWebhookInfo() + if err == nil && w.IsSet() { + return nil, errors.New("webhook was set, can't use polling") + } + + ch := make(chan Update, h.bufferSize) + + go func() { + for { + select { + case <-h.shutdownChannel: + close(ch) + return + default: + } + + updates, err := h.bot.GetUpdates(h.updateConfig) + if err != nil { + log.Println(err) + log.Println("Failed to get updates, retrying in 3 seconds...") + time.Sleep(time.Second * 3) + + continue + } + + for _, update := range updates { + if update.UpdateID >= h.updateConfig.Offset { + h.updateConfig.Offset = update.UpdateID + 1 + ch <- update + } + } + } + }() + + return ch, nil +} + +// Stop stops the go routine which receives updates +func (h *PollingHandler) Stop() { + if h.bot.GetConfig().GetDebug() { + log.Println("Stopping the update receiver routine...") + } + close(h.shutdownChannel) +} + +// ListenForWebhook registers a http handler for a webhook. +func (h *WebhookHandler) ListenForWebhook(pattern string) UpdatesChannel { + ch := make(chan Update, h.bufferSize) + + http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + update, err := UnmarshalUpdate(r) + if err != nil { + errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(errMsg) + return + } + + ch <- *update + }) + + return ch +} + +// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook. +func (h *WebhookHandler) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel { + ch := make(chan Update, h.bufferSize) + + func(w http.ResponseWriter, r *http.Request) { + defer close(ch) + + update, err := UnmarshalUpdate(r) + if err != nil { + errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(errMsg) + return + } + + ch <- *update + }(w, r) + + return ch +} + +// UnmarshalUpdate parses and returns update received via webhook +func UnmarshalUpdate(r *http.Request) (*Update, error) { + if r.Method != http.MethodPost { + err := errors.New("wrong HTTP method required POST") + return nil, err + } + + var update Update + err := json.NewDecoder(r.Body).Decode(&update) + if err != nil { + return nil, err + } + + return &update, nil +} diff --git a/example/example_bot.go b/example/example_bot.go index 3c64ae6..85ee5fb 100644 --- a/example/example_bot.go +++ b/example/example_bot.go @@ -22,17 +22,19 @@ func ExampleNewBotAPI() { } fmt.Printf("Authorized on account %s", usr.UserName) - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 - updates := bot.GetUpdatesChan(u) + updCh, err := tgbotapi.NewPollingHandler(bot, tgbotapi.NewUpdate(0)). + InitUpdatesChannel() + if err != nil { + panic(err) + } // Optional: wait for updates and clear them if you don't want to handle // a large backlog of old messages time.Sleep(time.Millisecond * 500) - updates.Clear() + updCh.Clear() - for update := range updates { + for update := range updCh { if update.Message == nil { continue } @@ -86,10 +88,12 @@ func ExampleNewWebhook() { fmt.Printf("failed to set webhook: %s", info.LastErrorMessage) } - updates := bot.ListenForWebhook("/" + cfg.GetToken()) + updCh := tgbotapi.NewWebhookHandler(bot). + ListenForWebhook("/bot") + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) - for update := range updates { + for update := range updCh { fmt.Printf("%+v\n", update) } } @@ -128,7 +132,7 @@ func ExampleWebhookHandler() { } http.HandleFunc("/"+cfg.GetToken(), func(w http.ResponseWriter, r *http.Request) { - update, err := bot.HandleUpdate(r) + update, err := tgbotapi.UnmarshalUpdate(r) if err != nil { fmt.Printf("%+v\n", err.Error()) } else { @@ -157,7 +161,10 @@ func ExampleInlineConfig() { u := tgbotapi.NewUpdate(0) u.Timeout = 60 - updates := bot.GetUpdatesChan(u) + updates, err := tgbotapi.NewPollingHandler(bot, u).InitUpdatesChannel() + if err != nil { + panic(err) + } for update := range updates { if update.InlineQuery == nil { // if no inline query, ignore it