skip to Main Content

I would like to have a little Telegram bot in python where a command is issued by the user, the bot asks a question and reacts depending on the answer. The official Telegram API mentions that this is possible using ForceReply(), e.g. for creating polls step by step, see here Official Telegram API # ForceReply.

I wrote the following code:

def test_function(update: Update, context: CallbackContext) -> None:
    msg = context.bot.send_message(chat_id=update.message.chat_id, 
                                   text="Please enter some text.",
                                   reply_markup=telegram.ForceReply(True))

if __name__ == '__main__':
    dispatcher.add_handler(CommandHandler("test", test_function))
    updater.start_polling()
    updater.idle()

So when /test is issued by the user, he is asked to enter some text and due to the ForceReply(True), the user is forced to reply to the message. But how do I get the result of this reply, i.e. the text replied by the user? There is no hint in the API docs and I also spent some time searching on the internet and on SO but didn’t find anything. Therefore, most likely the answer is simple and clear but I just don’t get it at the moment.

Any kind of help is appreciated.

2

Answers


  1. aiogram framework already solved your task

    Every step is a state of user.

    It’s called FSM (finite state machine).

    You don’t even need to do something with ForceReply.

    Example

    import logging
    
    import aiogram.utils.markdown as md
    from aiogram import Bot, Dispatcher, types
    from aiogram.contrib.fsm_storage.memory import MemoryStorage
    from aiogram.dispatcher import FSMContext
    from aiogram.dispatcher.filters import Text
    from aiogram.dispatcher.filters.state import State, StatesGroup
    from aiogram.types import ParseMode
    from aiogram.utils import executor
    
    
    bot = Bot(token='your:token')
    
    
    storage = MemoryStorage()  # external storage is supported!
    dp = Dispatcher(bot, storage=storage)
    
    
    # Defining available states
    class Form(StatesGroup):
        name = State()
        age = State()
        gender = State()
    
    
    @dp.message_handler(commands='start')
    async def cmd_start(message: types.Message):
        """
        Conversation's entry point
        """
        # Set state
        await Form.name.set()
        
        await message.reply("Hi there! What's your name?")
    
    
    # You can use state '*' if you need to handle all states
    @dp.message_handler(state='*', commands='cancel')
    @dp.message_handler(Text(equals='cancel', ignore_case=True), state='*')
    async def cancel_handler(message: types.Message, state: FSMContext):
        """
        Allow user to cancel any action
        """
        current_state = await state.get_state()
        if current_state is None:
            return
    
        # Cancel state and inform user about it
        await state.finish()
        # And remove keyboard (just in case)
        await message.reply('Cancelled.', reply_markup=types.ReplyKeyboardRemove())
    
    
    @dp.message_handler(state=Form.name)
    async def process_name(message: types.Message, state: FSMContext):
        """
        Process user name
        """
        async with state.proxy() as data:
            data['name'] = message.text
    
        await Form.next()
        await message.reply("How old are you?")
    
    
    # Check age. Age gotta be digit
    @dp.message_handler(lambda message: not message.text.isdigit(), state=Form.age)
    async def process_age_invalid(message: types.Message):
        """
        If age is invalid
        """
        return await message.reply("Age gotta be a number.nHow old are you? (digits only)")
    
    
    @dp.message_handler(lambda message: message.text.isdigit(), state=Form.age)
    async def process_age(message: types.Message, state: FSMContext):
        # Update state and data
        await Form.next()
        await state.update_data(age=int(message.text))
    
        # Configure ReplyKeyboardMarkup
        markup = types.ReplyKeyboardMarkup(resize_keyboard=True, selective=True)
        markup.add("Male", "Female")
        markup.add("Other")
    
        await message.reply("What is your gender?", reply_markup=markup)
    
    
    @dp.message_handler(lambda message: message.text not in ["Male", "Female", "Other"], state=Form.gender)
    async def process_gender_invalid(message: types.Message):
        """
        In this example gender has to be one of: Male, Female, Other.
        """
        return await message.reply("Bad gender name. Choose your gender from the keyboard.")
    
    
    @dp.message_handler(state=Form.gender)
    async def process_gender(message: types.Message, state: FSMContext):
        async with state.proxy() as data:
            data['gender'] = message.text
    
            # Remove keyboard
            markup = types.ReplyKeyboardRemove()
    
            # And send message
            await bot.send_message(
                message.chat.id,
                md.text(
                    md.text('Hi! Nice to meet you,', md.bold(data['name'])),
                    md.text('Age:', md.code(data['age'])),
                    md.text('Gender:', data['gender']),
                    sep='n',
                ),
                reply_markup=markup,
                parse_mode=ParseMode.MARKDOWN,
            )
    
        # Finish conversation
        await state.finish()
    
    
    if __name__ == '__main__':
        executor.start_polling(dp, skip_updates=True)
    
    Login or Signup to reply.
  2. You receive the answer as a general message (see any demo on echo-bot), and that test if it is a reply to your message. If it is, you can process the answer and proceed further.

    Example:

    # Mock database :-)
    question_id = None
    
    # This filter allows to catch only a reply-to messages.
    @dp.message_handler(aiogram.dispatcher.filters.IsReplyFilter(True))
    async def echo_task(message: types.Message):
        """Here I catch all replies to the messages to treat them as answers."""
        if message.reply_to_message.message_id == question_id:
            await bot.send_message(message.chat.id,
                                   f"Welcome, Mr(s) {message.text}")
        else:
            await bot.send_message(message.chat.id,
                                   "You'd better come to desk 11, sir(madam)")
        await bot.unpin_chat_message(message.chat.id, question_id)
    
    
    # Here we catch any general messages user typed.
    @dp.message_handler(aiogram.dispatcher.filters.IsReplyFilter(False))
    async def echo_task(message: types.Message):
        """Global receiver of the data we didn't handle yet."""
        global question_id
        bot_message = await bot.send_message(message.chat.id,
                                             "May I ask your name, please?",
                                             reply_markup=aiogram.types.ForceReply())
        question_id = bot_message.message_id
        await bot.pin_chat_message(message.chat.id, question_id)
    

    I pinned and unpinned questions as a part of an experiment – just drop it, if not needed.

    Dialog will look like
    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search