Skip to content
Back to top
View this page

# `conversationbot.py`

      1#!/usr/bin/env python
      2# pylint: disable=unused-argument
      3# This program is dedicated to the public domain under the CC0 license.
      4
      5"""
      6First, a few callback functions are defined. Then, those functions are passed to
      7the Application and registered at their respective places.
      8Then, the bot is started and runs until we press Ctrl-C on the command line.
      9
     10Usage:
     11Example of a bot-user conversation using ConversationHandler.
     12Send /start to initiate the conversation.
     13Press Ctrl-C on the command line or send a signal to the process to stop the
     14bot.
     15"""
     16
     17import logging
     18
     19from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
     20from telegram.ext import (
     21    Application,
     22    CommandHandler,
     23    ContextTypes,
     24    ConversationHandler,
     25    MessageHandler,
     26    filters,
     27)
     28
     29# Enable logging
     30logging.basicConfig(
     31    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
     32)
     33# set higher logging level for httpx to avoid all GET and POST requests being logged
     34logging.getLogger("httpx").setLevel(logging.WARNING)
     35
     36logger = logging.getLogger(__name__)
     37
     38GENDER, PHOTO, LOCATION, BIO = range(4)
     39
     40
     41async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
     42    """Starts the conversation and asks the user about their gender."""
     43    reply_keyboard = [["Boy", "Girl", "Other"]]
     44
     45    await update.message.reply_text(
     46        "Hi! My name is Professor Bot. I will hold a conversation with you. "
     47        "Send /cancel to stop talking to me.\n\n"
     48        "Are you a boy or a girl?",
     49        reply_markup=ReplyKeyboardMarkup(
     50            reply_keyboard, one_time_keyboard=True, input_field_placeholder="Boy or Girl?"
     51        ),
     52    )
     53
     54    return GENDER
     55
     56
     57async def gender(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
     58    """Stores the selected gender and asks for a photo."""
     59    user = update.message.from_user
     60    logger.info("Gender of %s: %s", user.first_name, update.message.text)
     61    await update.message.reply_text(
     62        "I see! Please send me a photo of yourself, "
     63        "so I know what you look like, or send /skip if you don't want to.",
     64        reply_markup=ReplyKeyboardRemove(),
     65    )
     66
     67    return PHOTO
     68
     69
     70async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
     71    """Stores the photo and asks for a location."""
     72    user = update.message.from_user
     73    photo_file = await update.message.photo[-1].get_file()
     74    await photo_file.download_to_drive("user_photo.jpg")
     75    logger.info("Photo of %s: %s", user.first_name, "user_photo.jpg")
     76    await update.message.reply_text(
     77        "Gorgeous! Now, send me your location please, or send /skip if you don't want to."
     78    )
     79
     80    return LOCATION
     81
     82
     83async def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
     84    """Skips the photo and asks for a location."""
     85    user = update.message.from_user
     86    logger.info("User %s did not send a photo.", user.first_name)
     87    await update.message.reply_text(
     88        "I bet you look great! Now, send me your location please, or send /skip."
     89    )
     90
     91    return LOCATION
     92
     93
     94async def location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
     95    """Stores the location and asks for some info about the user."""
     96    user = update.message.from_user
     97    user_location = update.message.location
     98    logger.info(
     99        "Location of %s: %f / %f", user.first_name, user_location.latitude, user_location.longitude
    100    )
    101    await update.message.reply_text(
    102        "Maybe I can visit you sometime! At last, tell me something about yourself."
    103    )
    104
    105    return BIO
    106
    107
    108async def skip_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    109    """Skips the location and asks for info about the user."""
    110    user = update.message.from_user
    111    logger.info("User %s did not send a location.", user.first_name)
    112    await update.message.reply_text(
    113        "You seem a bit paranoid! At last, tell me something about yourself."
    114    )
    115
    116    return BIO
    117
    118
    119async def bio(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    120    """Stores the info about the user and ends the conversation."""
    121    user = update.message.from_user
    122    logger.info("Bio of %s: %s", user.first_name, update.message.text)
    123    await update.message.reply_text("Thank you! I hope we can talk again some day.")
    124
    125    return ConversationHandler.END
    126
    127
    128async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    129    """Cancels and ends the conversation."""
    130    user = update.message.from_user
    131    logger.info("User %s canceled the conversation.", user.first_name)
    132    await update.message.reply_text(
    133        "Bye! I hope we can talk again some day.", reply_markup=ReplyKeyboardRemove()
    134    )
    135
    136    return ConversationHandler.END
    137
    138
    139def main() -> None:
    140    """Run the bot."""
    141    # Create the Application and pass it your bot's token.
    142    application = Application.builder().token("TOKEN").build()
    143
    144    # Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO
    145    conv_handler = ConversationHandler(
    146        entry_points=[CommandHandler("start", start)],
    147        states={
    148            # Use case-insensitive regex to accept gender input regardless of letter casing,
    149            # e.g., "boy", "BOY", "Girl", etc., will all be matched
    150            GENDER: [MessageHandler(filters.Regex("(?i)^(Boy|Girl|Other)$"), gender)],
    151            PHOTO: [MessageHandler(filters.PHOTO, photo), CommandHandler("skip", skip_photo)],
    152            LOCATION: [
    153                MessageHandler(filters.LOCATION, location),
    154                CommandHandler("skip", skip_location),
    155            ],
    156            BIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, bio)],
    157        },
    158        fallbacks=[CommandHandler("cancel", cancel)],
    159    )
    160
    161    application.add_handler(conv_handler)
    162
    163    # Run the bot until the user presses Ctrl-C
    164    application.run_polling(allowed_updates=Update.ALL_TYPES)
    165
    166
    167if __name__ == "__main__":
    168    main()

## State Diagram

            flowchart TB
        %% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart
        A(("/start")):::entryPoint -->|Hi! My name is Professor Bot...| B((GENDER)):::state
        B --> |"- Boy <br /> - Girl <br /> - Other"|C("(choice)"):::userInput
        C --> |I see! Please send me a photo...| D((PHOTO)):::state
        D --> E("/skip"):::userInput
        D --> F("(photo)"):::userInput
        E --> |I bet you look great!| G[\ /]:::userInput
        F --> |Gorgeous!| G[\ /]
        G --> |"Now, send me your location .."| H((LOCATION)):::state
        H --> I("/skip"):::userInput
        H --> J("(location)"):::userInput
        I --> |You seem a bit paranoid!| K[\" "/]:::userInput
        J --> |Maybe I can visit...| K
        K --> |"Tell me about yourself..."| L(("BIO")):::state
        L --> M("(text)"):::userInput
        M --> |"Thanks and bye!"| End(("END")):::termination
        classDef userInput  fill:#2a5279, color:#ffffff, stroke:#ffffff
        classDef state fill:#222222, color:#ffffff, stroke:#ffffff
        classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff
        classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff