This commit is contained in:
+64
-62
@@ -15,8 +15,8 @@ import (
|
||||
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||
)
|
||||
|
||||
// BotWebHookOpts configures Telegram webhook registration and the local HTTP server.
|
||||
type BotWebHookOpts struct {
|
||||
// BotWebhookOpts configures Telegram webhook registration and the local HTTP server.
|
||||
type BotWebhookOpts struct {
|
||||
Path string
|
||||
LocalPort int
|
||||
UseStatusPath bool
|
||||
@@ -30,9 +30,9 @@ type BotWebHookOpts struct {
|
||||
SecretToken string
|
||||
}
|
||||
|
||||
// NewBotWebHookOpts returns webhook options with the default path, local port, and max connections.
|
||||
func NewBotWebHookOpts() *BotWebHookOpts {
|
||||
return &BotWebHookOpts{
|
||||
// NewBotWebhookOpts returns webhook options with the default path, local port, and max connections.
|
||||
func NewBotWebhookOpts() *BotWebhookOpts {
|
||||
return &BotWebhookOpts{
|
||||
Path: "/",
|
||||
LocalPort: 8080,
|
||||
MaxConnections: 40,
|
||||
@@ -40,38 +40,38 @@ func NewBotWebHookOpts() *BotWebHookOpts {
|
||||
}
|
||||
|
||||
// SetPath sets the local HTTP path that receives Telegram webhook requests.
|
||||
func (opts *BotWebHookOpts) SetPath(path string) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetPath(path string) *BotWebhookOpts {
|
||||
opts.Path = path
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLocalPort sets the local HTTP port used by the webhook server.
|
||||
func (opts *BotWebHookOpts) SetLocalPort(port int) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetLocalPort(port int) *BotWebhookOpts {
|
||||
opts.LocalPort = port
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetUseStatusPath enables or disables the optional /status endpoint.
|
||||
// A non-empty SecretToken is required when this endpoint is enabled.
|
||||
func (opts *BotWebHookOpts) SetUseStatusPath(use bool) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetUseStatusPath(use bool) *BotWebhookOpts {
|
||||
opts.UseStatusPath = use
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetURL sets the public base URL Telegram should call for incoming updates.
|
||||
func (opts *BotWebHookOpts) SetURL(url string) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetURL(url string) *BotWebhookOpts {
|
||||
opts.URL = url
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetCertificate sets the self-signed webhook certificate bytes to upload.
|
||||
func (opts *BotWebHookOpts) SetCertificate(certificate []byte) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetCertificate(certificate []byte) *BotWebhookOpts {
|
||||
opts.Certificate = certificate
|
||||
return opts
|
||||
}
|
||||
|
||||
// MustLoadCertificate loads a webhook certificate from disk and panics on failure.
|
||||
func (opts *BotWebHookOpts) MustLoadCertificate(filename string) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) MustLoadCertificate(filename string) *BotWebhookOpts {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -87,37 +87,37 @@ func (opts *BotWebHookOpts) MustLoadCertificate(filename string) *BotWebHookOpts
|
||||
}
|
||||
|
||||
// SetIPAddress sets the fixed IP address Telegram should use for webhook delivery.
|
||||
func (opts *BotWebHookOpts) SetIPAddress(ip string) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetIPAddress(ip string) *BotWebhookOpts {
|
||||
opts.IPAddress = ip
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetMaxConnections sets Telegram's maximum number of simultaneous webhook connections.
|
||||
func (opts *BotWebHookOpts) SetMaxConnections(max int8) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetMaxConnections(max int8) *BotWebhookOpts {
|
||||
opts.MaxConnections = max
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetAllowedUpdates sets the Telegram update types that should be delivered to the webhook.
|
||||
func (opts *BotWebHookOpts) SetAllowedUpdates(updates ...tgapi.UpdateType) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetAllowedUpdates(updates ...tgapi.UpdateType) *BotWebhookOpts {
|
||||
opts.AllowedUpdates = append([]tgapi.UpdateType(nil), updates...)
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetDropPendingUpdates configures whether Telegram should drop pending updates while setting the webhook.
|
||||
func (opts *BotWebHookOpts) SetDropPendingUpdates(drop bool) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetDropPendingUpdates(drop bool) *BotWebhookOpts {
|
||||
opts.DropPendingUpdates = drop
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetSecretToken sets the secret token expected in Telegram webhook requests.
|
||||
// The same token is also required to access /status when that endpoint is enabled.
|
||||
func (opts *BotWebHookOpts) SetSecretToken(secretToken string) *BotWebHookOpts {
|
||||
func (opts *BotWebhookOpts) SetSecretToken(secretToken string) *BotWebhookOpts {
|
||||
opts.SecretToken = secretToken
|
||||
return opts
|
||||
}
|
||||
|
||||
// RunWebHookWithContext registers the webhook and serves incoming updates until ctx is canceled.
|
||||
// RunWebhookWithContext registers the webhook and serves incoming updates until ctx is canceled.
|
||||
//
|
||||
// The bot uses the same update queue, worker pool, runner startup, and single-use lifecycle
|
||||
// guarantees as RunWithContext. When opts.AllowedUpdates is empty, the bot-level update types
|
||||
@@ -126,9 +126,9 @@ func (opts *BotWebHookOpts) SetSecretToken(secretToken string) *BotWebHookOpts {
|
||||
//
|
||||
// When two TLS files are provided, the method serves HTTPS using the existing key-then-cert
|
||||
// argument order.
|
||||
func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOpts, tlsFiles ...string) error {
|
||||
func (bot *Bot[T]) RunWebhookWithContext(ctx context.Context, opts *BotWebhookOpts, tlsFiles ...string) error {
|
||||
if opts == nil {
|
||||
return errors.New("nil BotWebHookOpts")
|
||||
return errors.New("nil BotWebhookOpts")
|
||||
}
|
||||
if len(bot.prefixes) == 0 {
|
||||
return ErrNoPrefixes
|
||||
@@ -137,44 +137,42 @@ func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOp
|
||||
return ErrNoPlugins
|
||||
}
|
||||
if opts.URL == "" {
|
||||
return errors.New("empty BotWebHookOpts.URL")
|
||||
return errors.New("empty BotWebhookOpts.URL")
|
||||
}
|
||||
if opts.MaxConnections > 100 || opts.MaxConnections <= 0 {
|
||||
return errors.New("BotWebHookOpts.MaxConnections must between 1 and 100")
|
||||
return errors.New("BotWebhookOpts.MaxConnections must between 1 and 100")
|
||||
}
|
||||
if err := validateWebhookPath(opts.Path, opts.UseStatusPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.UseStatusPath && opts.SecretToken == "" {
|
||||
return errors.New("BotWebHookOpts.SecretToken required when status path is enabled")
|
||||
return errors.New("BotWebhookOpts.SecretToken required when status path is enabled")
|
||||
}
|
||||
if err := validateWebhookTLSFiles(tlsFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bot.webHookLogger = utils.CreateLogger("WEBHOOK", bot.GetLoggerLevel(), bot.logFormat, bot.logFormatter)
|
||||
bot.addTokenReplacer(bot.webHookLogger)
|
||||
if opts.SecretToken == "" {
|
||||
bot.webHookLogger.Warnln("Bot webhook secret token empty. It's VERY recommended to set secret.")
|
||||
}
|
||||
|
||||
if opts.Certificate != nil && bot.uploader == nil {
|
||||
return errors.New("bot uploader nil, but certificate set")
|
||||
}
|
||||
|
||||
return bot.runWebhookRuntime(ctx, func(runCtx context.Context) error {
|
||||
if opts.SecretToken == "" {
|
||||
bot.webhookLogger.Warnln("Bot webhook secret token empty. It's VERY recommended to set secret.")
|
||||
}
|
||||
|
||||
i, err := bot.api.GetWebhookInfoWithContext(runCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i.URL == "" {
|
||||
bot.webHookLogger.Warnln("API returned webhook info with empty URL. There may be a long-poll")
|
||||
bot.webhookLogger.Warnln("API returned webhook info with empty URL. There may be a long-poll")
|
||||
} else {
|
||||
_, err = bot.api.DeleteWebhookWithContext(runCtx, tgapi.DeleteWebhook{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bot.webHookLogger.Infof("Bot webhook deleted: %s", i.URL)
|
||||
bot.webhookLogger.Infof("Bot webhook deleted: %s", i.URL)
|
||||
}
|
||||
|
||||
allowedUpdates := bot.webhookAllowedUpdates(opts)
|
||||
@@ -207,48 +205,48 @@ func (bot *Bot[T]) RunWebHookWithContext(ctx context.Context, opts *BotWebHookOp
|
||||
}
|
||||
|
||||
if len(tlsFiles) == 2 {
|
||||
return bot.runWebHookTLS(runCtx, opts, tlsFiles[0], tlsFiles[1])
|
||||
return bot.runWebhookTLS(runCtx, opts, tlsFiles[0], tlsFiles[1])
|
||||
}
|
||||
|
||||
return bot.runWebHook(runCtx, opts)
|
||||
return bot.runWebhook(runCtx, opts)
|
||||
})
|
||||
}
|
||||
|
||||
// RunWebHook starts the webhook runtime with a background context.
|
||||
// RunWebhook starts the webhook runtime with a background context.
|
||||
//
|
||||
// It is shorthand for RunWebHookWithContext(context.Background(), opts, tlsFiles...).
|
||||
func (bot *Bot[T]) RunWebHook(opts *BotWebHookOpts, tlsFiles ...string) error {
|
||||
return bot.RunWebHookWithContext(context.Background(), opts, tlsFiles...)
|
||||
// It is shorthand for RunWebhookWithContext(context.Background(), opts, tlsFiles...).
|
||||
func (bot *Bot[T]) RunWebhook(opts *BotWebhookOpts, tlsFiles ...string) error {
|
||||
return bot.RunWebhookWithContext(context.Background(), opts, tlsFiles...)
|
||||
}
|
||||
|
||||
// CloseWebHook removes the current Telegram webhook registration.
|
||||
// CloseWebhook removes the current Telegram webhook registration.
|
||||
//
|
||||
// It is separate from Close, which only releases local resources.
|
||||
// Call it before switching a deployment from webhook delivery to polling.
|
||||
func (bot *Bot[T]) CloseWebHook() error {
|
||||
func (bot *Bot[T]) CloseWebhook() error {
|
||||
var e []error
|
||||
if bot.api == nil {
|
||||
e = append(e, errors.New("bot api nil"))
|
||||
} else {
|
||||
if _, err := bot.api.DeleteWebhook(tgapi.DeleteWebhook{}); err != nil {
|
||||
if bot.webHookLogger != nil {
|
||||
bot.webHookLogger.Errorf("Failed to close webhook: %s", err.Error())
|
||||
if bot.webhookLogger != nil {
|
||||
bot.webhookLogger.Errorf("Failed to close webhook: %s", err.Error())
|
||||
} else if bot.logger != nil {
|
||||
bot.logger.Errorf("Failed to close webhook: %s", err.Error())
|
||||
}
|
||||
e = append(e, err)
|
||||
}
|
||||
}
|
||||
if bot.webHookLogger != nil {
|
||||
if err := bot.webHookLogger.Close(); err != nil {
|
||||
if bot.webhookLogger != nil {
|
||||
if err := bot.webhookLogger.Close(); err != nil {
|
||||
e = append(e, err)
|
||||
}
|
||||
bot.webHookLogger = nil
|
||||
bot.webhookLogger = nil
|
||||
}
|
||||
return errors.Join(e...)
|
||||
}
|
||||
|
||||
func (bot *Bot[T]) webhookAllowedUpdates(opts *BotWebHookOpts) []tgapi.UpdateType {
|
||||
func (bot *Bot[T]) webhookAllowedUpdates(opts *BotWebhookOpts) []tgapi.UpdateType {
|
||||
if len(opts.AllowedUpdates) > 0 {
|
||||
return append([]tgapi.UpdateType(nil), opts.AllowedUpdates...)
|
||||
}
|
||||
@@ -264,6 +262,10 @@ func (bot *Bot[T]) runWebhookRuntime(ctx context.Context, run func(context.Conte
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if bot.webhookLogger == nil {
|
||||
bot.webhookLogger = utils.CreateLogger("WEBHOOK", bot.GetLoggerLevel(), bot.logFormat, bot.logFormatter)
|
||||
}
|
||||
bot.addTokenReplacer(bot.webhookLogger)
|
||||
bot.ExecRunners(runCtx)
|
||||
|
||||
workersDone := make(chan struct{})
|
||||
@@ -315,12 +317,12 @@ func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.
|
||||
var up tgapi.Update
|
||||
if err := json.Unmarshal(data, &up); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
bot.webHookLogger.Errorln(err)
|
||||
bot.webhookLogger.Errorln(err)
|
||||
return
|
||||
}
|
||||
bot.webHookLogger.Debugf("UPDATE id=%d type=%s size=%d from=%s", up.UpdateID, up.Type, len(data), r.RemoteAddr)
|
||||
bot.webhookLogger.Debugf("UPDATE id=%d type=%s size=%d from=%s", up.UpdateID, up.Type, len(data), r.RemoteAddr)
|
||||
if err := bot.enqueueUpdate(ctx, up); err != nil {
|
||||
bot.webHookLogger.Errorln(err)
|
||||
bot.webhookLogger.Errorln(err)
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
@@ -328,7 +330,7 @@ func updateHandler[T any](ctx context.Context, bot *Bot[T], secret string) http.
|
||||
}
|
||||
}
|
||||
|
||||
func statusHandler[T any](bot *Bot[T], opts *BotWebHookOpts) http.HandlerFunc {
|
||||
func statusHandler[T any](bot *Bot[T], opts *BotWebhookOpts) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := ""
|
||||
if r.Header.Get("Authorization") != "" {
|
||||
@@ -343,24 +345,24 @@ func statusHandler[T any](bot *Bot[T], opts *BotWebHookOpts) http.HandlerFunc {
|
||||
|
||||
i, err := bot.api.GetWebhookInfoWithContext(r.Context())
|
||||
if err != nil {
|
||||
bot.webHookLogger.Errorln(err)
|
||||
bot.webhookLogger.Errorln(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(i, "", " ")
|
||||
if err != nil {
|
||||
bot.webHookLogger.Errorln(err)
|
||||
bot.webhookLogger.Errorln(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := fmt.Fprint(w, string(data)); err != nil {
|
||||
bot.webHookLogger.Errorln(err)
|
||||
bot.webhookLogger.Errorln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bot *Bot[T]) newWebHookMux(ctx context.Context, opts *BotWebHookOpts) *http.ServeMux {
|
||||
func (bot *Bot[T]) newWebhookMux(ctx context.Context, opts *BotWebhookOpts) *http.ServeMux {
|
||||
r := http.NewServeMux()
|
||||
if opts.UseStatusPath {
|
||||
r.HandleFunc("/status", statusHandler(bot, opts))
|
||||
@@ -368,10 +370,10 @@ func (bot *Bot[T]) newWebHookMux(ctx context.Context, opts *BotWebHookOpts) *htt
|
||||
r.HandleFunc(opts.Path, updateHandler(ctx, bot, opts.SecretToken))
|
||||
return r
|
||||
}
|
||||
func (bot *Bot[T]) runWebHook(ctx context.Context, opts *BotWebHookOpts) error {
|
||||
func (bot *Bot[T]) runWebhook(ctx context.Context, opts *BotWebhookOpts) error {
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opts.LocalPort),
|
||||
Handler: bot.newWebHookMux(ctx, opts),
|
||||
Handler: bot.newWebhookMux(ctx, opts),
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
@@ -384,7 +386,7 @@ func (bot *Bot[T]) runWebHook(ctx context.Context, opts *BotWebHookOpts) error {
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
bot.webHookLogger.Infoln(fmt.Sprintf("Bot WebHook started at %s; waiting for updates at %s", srv.Addr, opts.URL))
|
||||
bot.webhookLogger.Infoln(fmt.Sprintf("Bot Webhook started at %s; waiting for updates at %s", srv.Addr, opts.URL))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -401,10 +403,10 @@ func (bot *Bot[T]) runWebHook(ctx context.Context, opts *BotWebHookOpts) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
func (bot *Bot[T]) runWebHookTLS(ctx context.Context, opts *BotWebHookOpts, key, cert string) error {
|
||||
func (bot *Bot[T]) runWebhookTLS(ctx context.Context, opts *BotWebhookOpts, key, cert string) error {
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opts.LocalPort),
|
||||
Handler: bot.newWebHookMux(ctx, opts),
|
||||
Handler: bot.newWebhookMux(ctx, opts),
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
@@ -417,7 +419,7 @@ func (bot *Bot[T]) runWebHookTLS(ctx context.Context, opts *BotWebHookOpts, key,
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
bot.webHookLogger.Infoln(fmt.Sprintf("Bot webhook started with TLS(%s, %s) at %s; waiting for updates at %s", key, cert, srv.Addr, opts.URL))
|
||||
bot.webhookLogger.Infoln(fmt.Sprintf("Bot webhook started with TLS(%s, %s) at %s; waiting for updates at %s", key, cert, srv.Addr, opts.URL))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -436,16 +438,16 @@ func (bot *Bot[T]) runWebHookTLS(ctx context.Context, opts *BotWebHookOpts, key,
|
||||
}
|
||||
func validateWebhookPath(path string, useStatusPath bool) error {
|
||||
if path == "" {
|
||||
return errors.New("empty BotWebHookOpts.Path")
|
||||
return errors.New("empty BotWebhookOpts.Path")
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return errors.New("BotWebHookOpts.Path must start with '/'")
|
||||
return errors.New("BotWebhookOpts.Path must start with '/'")
|
||||
}
|
||||
if strings.Contains(path, "?") || strings.Contains(path, "#") {
|
||||
return errors.New("BotWebHookOpts.Path must not contain query or fragment")
|
||||
return errors.New("BotWebhookOpts.Path must not contain query or fragment")
|
||||
}
|
||||
if useStatusPath && path == "/status" {
|
||||
return errors.New("BotWebHookOpts.Path must not be '/status' when status path is enabled")
|
||||
return errors.New("BotWebhookOpts.Path must not be '/status' when status path is enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user