Decouple bot handlers.

This commit is contained in:
Ilja Lapkovskis 2024-01-30 21:46:49 +02:00
parent 1b1af39b61
commit 1ef07c39bf
No known key found for this signature in database
GPG key ID: 53D2AA4F0D1079C4
3 changed files with 181 additions and 135 deletions

138
bot.go
View file

@ -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.
@ -46,8 +42,6 @@ func NewBotWithClient(config BotConfigI, client HTTPClientI) *BotAPI {
bot := &BotAPI{
config: config.(*BotConfig),
client: client,
buffer: DefaultBufferSize,
shutdownChannel: make(chan interface{}),
}
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.

149
bot_handler.go Normal file
View file

@ -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
}

View file

@ -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