tucnak / telebot

Telebot is a Telegram bot framework in Go.
MIT License
3.77k stars 440 forks source link

How to send an alert using bot instance? #663

Closed jiajunhuang closed 3 months ago

jiajunhuang commented 4 months ago

In normal case, we can send alert by: c.Respond() to send an alert, but as the QPS increase higher, I need to queue the respond and send the respond later, how can I send it?

I've tried to do it like this:

first, save the message_id & callback_id from c:

        req := types.RespondRender{
                CallbackID: callback.ID,
                MessageID:  callback.MessageID,
                Text:       text,
                ShowAlert:  showAlert,
        }
err = bot.Respond(
         &tb.Callback{ID: req.CallbackID, MessageID: req.MessageID},
         &tb.CallbackResponse{Text: req.Text, ShowAlert: req.ShowAlert},
)

the API does not return any error, but the alert dialog does not appear, too.

demget commented 4 months ago

You can't show an alert out of nowhere, you have to respond to the user's action like pressing a callback button.

https://core.telegram.org/bots/api#callbackquery

jiajunhuang commented 4 months ago

But I don't see any difference between c.Respond and bot.Respond, as c.Respond is just an wrapper:

func (c *nativeContext) Respond(resp ...*CallbackResponse) error {
    if c.u.Callback == nil {
        return errors.New("telebot: context callback is nil")
    }
    return c.b.Respond(c.u.Callback, resp...)
}

// where c.b is a *Bot instance

and bot.Respond is just calling answerCallbackQuery with callback.ID:

// Respond sends a response for a given callback query. A callback can
// only be responded to once, subsequent attempts to respond to the same callback
// will result in an error.
//
// Example:
//
//  b.Respond(c)
//  b.Respond(c, response)
func (b *Bot) Respond(c *Callback, resp ...*CallbackResponse) error {
    var r *CallbackResponse
    if resp == nil {
        r = &CallbackResponse{}
    } else {
        r = resp[0]
    }

    r.CallbackID = c.ID
    _, err := b.Raw("answerCallbackQuery", r)
    return err
}

So it should work if I call the Respond method of bot instance directly like

bot, err := tb.NewBot(...)
err = bot.Respond(
         &tb.Callback{ID: req.CallbackID},
         &tb.CallbackResponse{Text: req.Text, ShowAlert: req.ShowAlert},
)

I've check the response from server by enable verbose mode, which returns ok, but the alert dialog does not appear. I've check the response from server when I directly call c.Respond too, which returns ok, but the alert dialog appears, but the question is both the request and response structure has no difference, where cause the different behavior?

jiajunhuang commented 4 months ago

Here is a testing script by using python-telegram-bot, it works like code above:

import asyncio

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (ApplicationBuilder, CallbackContext,
                          CallbackQueryHandler, CommandHandler, Updater)

async def start(update: Update, context: CallbackContext) -> None:
    keyboard = [
        [InlineKeyboardButton("Click me!", callback_data='welcome')],
    ]

    reply_markup = InlineKeyboardMarkup(keyboard)

    await update.message.reply_text('Welcome!', reply_markup=reply_markup)

async def another_coroutine_alert(bot, query):
    await asyncio.sleep(5)
    await bot.answer_callback_query(query.id, text="aha", show_alert=True)

async def button(update: Update, context: CallbackContext) -> None:
    query = update.callback_query

    # run in another coroutine and do not await it
    asyncio.ensure_future(another_coroutine_alert(context.bot, query))

def main() -> None:
    # Create the Updater and pass it your bot's token.
    bot = ApplicationBuilder().token('<YOUR BOT TOKEN HERE>').build()

    bot.add_handler(CommandHandler('start', start))
    bot.add_handler(CallbackQueryHandler(button))

    # Start the Bot
    bot.run_polling()

if __name__ == '__main__':
    main()

ensure_future let's us execute a coroutine in the background, without explicitly waiting for it to finish, and in the another_coroutine_alert, we wait for 5 seconds to let the button coroutine to be finished.

I've test it, and it works, the bot show an alert dialog after 5 seconds.

image

jiajunhuang commented 3 months ago

Finally, I've found the reason. a callback can't be respond more than once. I've add a empty c.Respond in middleware to ensure that every button will be respond(so the client will not show a progress bar too long), after remove the empty c.Respond, I can send respond async, and the client will show an alert dialog.