Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hello everyone,

I'm practicing in writing the bot for telegram group that functions on the following "algorythm'/conditions:

  • group is public, but each join requires approve of admin (to trigger request join event)
  • bot is already in group, as admin, with permission to add users via invite link
  • when new user requests to join the group (clicks "Apply to join the group"), bot should:
    • start a separate (private) chat with user, with asking to solve the riddle (a.k.a. captcha)
    • wait or user's input
    • compare it with expected result
      • if it is correct - approve
      • otherwise - decline
    • clear the private chat

I strugle with processing user input from the created private chat. My "bot" creates the private conversation with newcomer at join request, sends to the candidate the message, but doesn't accept the responce. The logging doesn't capture the fact that user is responding.

The currenc code is:

from os import getenv
from enum import Enum
import logging

from telegram import Update, Bot
from telegram.ext import ApplicationBuilder, CommandHandler, ChatJoinRequestHandler, ConversationHandler, MessageHandler
from telegram.ext import Updater, ContextTypes, filters

passphrase=r"I want to join the channel"

TOKEN = getenv('TELEGRAM_BOT_TOKEN')

logging.basicConfig(
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        level=logging.INFO)

class States(Enum):
    WAITING_FOR_VERIFICATION = 1

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text(f'I am the bot, type me something!')

# Processing user's join request
async def join_request(update: Update, context: ContextTypes.DEFAULT_TYPE):

    logging.info(f"Captured attempt of user to join the group.\n\tUser : {update.effective_user.id}\n\tGroup: {update.effective_chat.id}")

    # sending user a private message
    await context.bot.send_message(
        chat_id=update.effective_user.id,text=
        f"Dummy captcha. Just respond '{passphrase}' (without brackets) to this message"
    )

    # Store chat join request details in context for later use
    context.user_data['join_request_chat_id'] = update.effective_chat.id
    context.user_data['join_request_user_id'] = update.effective_user.id
    
    return States.WAITING_FOR_VERIFICATION

async def verify_user_response(update: Update, context: ContextTypes.DEFAULT_TYPE):
    logging.info(f"Verification state triggered")
    logging.info(f"Update details: {update}")
    logging.info(f"Context user data: {context.user_data}")

    user_response = update.message.text
    logging.info(f"Captured responce of user : {update.effective_user.id}\n\tResponce: {user_response}")
    
    if user_response == passphrase:
        # Approve join request using stored details
        await context.bot.approve_chat_join_request(
            chat_id=context.user_data['join_request_chat_id'], 
            user_id=context.user_data['join_request_user_id']
        )
        await update.message.reply_text("Verification successful! You'll be forwarded to group now.")
        logging.info(f"User passed the verification and added to group:\n\t {update.effective_user.id}")
        return ConversationHandler.END
    else:
        # Decline join request
        await context.bot.decline_chat_join_request(
            chat_id=context.user_data['join_request_chat_id'], 
            user_id=context.user_data['join_request_user_id']
        )
        await update.message.reply_text("Verification failed. You cannot join the group.")
        logging.info(f"User FAILED the verification and NOT added to group:\n\t {update.effective_user.id}")
        return ConversationHandler.END


if __name__ == '__main__':

    default_bot = Bot(TOKEN)

    logging.info("Succesfully retrieved token")

    app = ApplicationBuilder().bot(default_bot).build()
    
    logging.info("Succesfully created application")

    # Create a handler for private mesage to user

    conv_handler = ConversationHandler(
        entry_points=[ChatJoinRequestHandler(join_request)],
        states={
            States.WAITING_FOR_VERIFICATION: [
                # Only plain text, no edited messages, no commands in privat conversation
                MessageHandler( filters.TEXT & ~filters.COMMAND & ~filters.UpdateType.EDITED_MESSAGE, verify_user_response)
            ]
        },
        fallbacks=[]
    )

    # Commands    
    app.add_handler(CommandHandler("start", start))

    # Conversations
    app.add_handler(conv_handler)

    # Finally run!
    
    app.run_polling(allowed_updates=Update.ALL_TYPES)

Please, advice.

Used versions:

  • Python 3.13.1
  • python-telegram-bot 21.10
  • Running from under venv and VSCodium
You must be logged in to vote

The Problem: Context Mismatch
In python-telegram-bot, a ConversationHandler tracks the state of a conversation based on a "key". By default, this key is a combination of (Chat ID + User ID).

The Request (Entry Point): The Join Request happens in the Group.

Context: (Group_ID, User_ID)

The bot saves the state WAITING_FOR_VERIFICATION under this key.

The Response: The user replies in a Private Chat (DM).

Context: (Private_Chat_ID, User_ID)

The bot looks for a state under this new key, finds nothing, and ignores the message.

The Solution
You must tell the ConversationHandler to ignore the "Chat ID" and track the state based only on the User ID. This allows the conversation to start in the Gr…

Replies: 2 comments · 3 replies

Comment options

The conversation handler is bound to the group chat, since per_chat is True. I would personally probably make a conversation handler with a message handler with a user filter to which I add a user id when I expect a captcha response (and remove when the conversation flow is done).

If you want to be able to debug these issues yourself, set the logging level to debug to see where the update is handled (or in this case, isn't)

You must be logged in to vote
2 replies
@YePererva
Comment options

Any example for such approach in WIKI or documentation?

@Poolitzer
Comment options

Not that I can think of no.

Comment options

The Problem: Context Mismatch
In python-telegram-bot, a ConversationHandler tracks the state of a conversation based on a "key". By default, this key is a combination of (Chat ID + User ID).

The Request (Entry Point): The Join Request happens in the Group.

Context: (Group_ID, User_ID)

The bot saves the state WAITING_FOR_VERIFICATION under this key.

The Response: The user replies in a Private Chat (DM).

Context: (Private_Chat_ID, User_ID)

The bot looks for a state under this new key, finds nothing, and ignores the message.

The Solution
You must tell the ConversationHandler to ignore the "Chat ID" and track the state based only on the User ID. This allows the conversation to start in the Group and finish in the Private Chat.

Step 1: Modify ConversationHandler
Set per_chat=False when defining your handler.

Step 2: Add a Filter (Safety)
Since per_chat=False makes the conversation global for that user, you should ensure the bot only accepts the password in the Private Chat, not if the user accidentally types it in another public group the bot manages. Add filters.ChatType.PRIVATE.

Fixed Code
Here is your corrected main block (the rest of your logic handles the data correctly):

Python

if name == 'main':
default_bot = Bot(TOKEN)
app = ApplicationBuilder().bot(default_bot).build()

# Create the ConversationHandler with per_chat=False
conv_handler = ConversationHandler(
    entry_points=[ChatJoinRequestHandler(join_request)],
    states={
        States.WAITING_FOR_VERIFICATION: [
            MessageHandler(
                # 1. Filter checks for Text AND Private Chat
                filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, 
                verify_user_response
            )
        ]
    },
    fallbacks=[],
    # 2. CRITICAL FIX: Track state by User only, ignoring which chat the update came from
    per_chat=False 
)

app.add_handler(CommandHandler("start", start))
app.add_handler(conv_handler)

# Remove allowed_updates=Update.ALL_TYPES if it causes errors, 
# or ensure you import Update correctly. Default is usually fine.
app.run_polling()
  1. A Note on "User Data"
    Your logic for storing the group ID is correct:

Python

context.user_data['join_request_chat_id'] = update.effective_chat.id
Because we switched to per_chat=False, context.user_data is tied specifically to the User. When the user replies in the DM, context.user_data will still contain the join_request_chat_id you saved during the join request, allowing approve_chat_join_request to work perfectly.

You must be logged in to vote
1 reply
@YePererva
Comment options

Thank you! It passes users now. Would you be so kind to add the instuction how to delete the private chat that was created after it served its function?

I assume it should be done right above the line return ConversationHandler.END ?

Answer selected by YePererva
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
⁉️
Q&A
Labels
None yet
3 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.