diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..54441d6 --- /dev/null +++ b/webhook.go @@ -0,0 +1,140 @@ +package telebot + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// A WebhookTLS specifies the path to a key and a cert so the poller can open +// a TLS listener +type WebhookTLS struct { + Key string + Cert string +} + +// A WebhookEndpoint describes the endpoint to which telegram will send its requests. +// This must be a public URL and can be a loadbalancer or something similar. If the +// endpoint uses TLS and the certificate is selfsigned you have to add the certificate +// path of this certificate so telegram will trust it. This field can be ignored if you +// have a trusted certifcate (letsencrypt, ...). +type WebhookEndpoint struct { + PublicURL string + Cert string +} + +// A Webhook configures the poller for webhooks. It opens a port on the given +// listen adress. If TLS is filled, the listener will use the key and cert to open +// a secure port. Otherwise it will use plain HTTP. +// If you have a loadbalancer ore other infrastructure in front of your service, you +// must fill the Endpoint structure so this poller will send this data to telegram. If +// you leave these values empty, your local adress will be sent to telegram which is mostly +// not what you want (at least while developing). If you have a single instance of your +// bot you should consider to use the LongPoller instead of a WebHook. +// If you are sure to publish your +type Webhook struct { + Listen string + TLS *WebhookTLS + Endpoint *WebhookEndpoint + dest chan<- Update + bot *Bot +} + +type registerResult struct { + Ok bool `json:"ok"` + ErrorCode int `json:"error_code"` + Description string `json:"description"` +} + +func (h *Webhook) getFiles() map[string]string { + m := make(map[string]string) + if h.TLS != nil { + m["certificate"] = h.TLS.Cert + } + // check if it is overwritten by an endpoint + if h.Endpoint != nil { + if h.Endpoint.Cert == "" { + // this can be the case if there is a loadbalancer or reverseproxy in + // front with a public cert. in this case we do not need to upload it + // to telegram. we delete the certificate from the map, because someone + // can have an internal TLS listener with a private cert + delete(m, "certificate") + } else { + // someone configured a certificate + m["certificate"] = h.Endpoint.Cert + } + } + return m +} + +func (h *Webhook) getParams() map[string]string { + param := make(map[string]string) + if h.TLS != nil { + param["url"] = fmt.Sprintf("https://%s", h.Listen) + } else { + // this will not work with telegram, they want TLS + // but i allow this because telegram will send an error + // when you register this hook. in their docs they write + // that port 80/http is allowed ... + param["url"] = fmt.Sprintf("http://%s", h.Listen) + } + if h.Endpoint != nil { + param["url"] = h.Endpoint.PublicURL + } + return param +} + +func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) { + res, err := b.sendFiles("setWebhook", h.getFiles(), h.getParams()) + if err != nil { + b.debug(fmt.Errorf("setWebhook failed %q: %v", string(res), err)) + close(stop) + return + } + var result registerResult + err = json.Unmarshal(res, &result) + if err != nil { + b.debug(fmt.Errorf("bad json data %q: %v", string(res), err)) + close(stop) + return + } + if !result.Ok { + b.debug(fmt.Errorf("cannot register webhook: %s", result.Description)) + close(stop) + return + } + s := &http.Server{ + Addr: h.Listen, + Handler: h, + } + + go func(stop chan struct{}) { + <-stop + close(stop) + s.Shutdown(context.Background()) + }(stop) + + // store the variables so the HTTP-handler can use 'em + h.dest = dest + h.bot = b + + if h.TLS != nil { + s.ListenAndServeTLS(h.TLS.Cert, h.TLS.Key) + } else { + s.ListenAndServe() + } + +} + +// The handler simply reads the update from the body of the requests +// and writes them to the update channel. +func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var update Update + err := json.NewDecoder(r.Body).Decode(&update) + if err != nil { + h.bot.debug(fmt.Errorf("cannot decode update: %v", err)) + return + } + h.dest <- update +}