contexttypesbot.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"""
  6Simple Bot to showcase `telegram.ext.ContextTypes`.
  7
  8Usage:
  9Press Ctrl-C on the command line or send a signal to the process to stop the
 10bot.
 11"""
 12
 13import logging
 14from collections import defaultdict
 15
 16from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
 17from telegram.constants import ParseMode
 18from telegram.ext import (
 19    Application,
 20    CallbackContext,
 21    CallbackQueryHandler,
 22    CommandHandler,
 23    ContextTypes,
 24    ExtBot,
 25    TypeHandler,
 26)
 27
 28# Enable logging
 29logging.basicConfig(
 30    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
 31)
 32# set higher logging level for httpx to avoid all GET and POST requests being logged
 33logging.getLogger("httpx").setLevel(logging.WARNING)
 34
 35logger = logging.getLogger(__name__)
 36
 37
 38class ChatData:
 39    """Custom class for chat_data. Here we store data per message."""
 40
 41    def __init__(self) -> None:
 42        self.clicks_per_message: defaultdict[int, int] = defaultdict(int)
 43
 44
 45# The [ExtBot, dict, ChatData, dict] is for type checkers like mypy
 46class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
 47    """Custom class for context."""
 48
 49    def __init__(
 50        self,
 51        application: Application,
 52        chat_id: int | None = None,
 53        user_id: int | None = None,
 54    ):
 55        super().__init__(application=application, chat_id=chat_id, user_id=user_id)
 56        self._message_id: int | None = None
 57
 58    @property
 59    def bot_user_ids(self) -> set[int]:
 60        """Custom shortcut to access a value stored in the bot_data dict"""
 61        return self.bot_data.setdefault("user_ids", set())
 62
 63    @property
 64    def message_clicks(self) -> int | None:
 65        """Access the number of clicks for the message this context object was built for."""
 66        if self._message_id:
 67            return self.chat_data.clicks_per_message[self._message_id]
 68        return None
 69
 70    @message_clicks.setter
 71    def message_clicks(self, value: int) -> None:
 72        """Allow to change the count"""
 73        if not self._message_id:
 74            raise RuntimeError("There is no message associated with this context object.")
 75        self.chat_data.clicks_per_message[self._message_id] = value
 76
 77    @classmethod
 78    def from_update(cls, update: object, application: "Application") -> "CustomContext":
 79        """Override from_update to set _message_id."""
 80        # Make sure to call super()
 81        context = super().from_update(update, application)
 82
 83        if context.chat_data and isinstance(update, Update) and update.effective_message:
 84            # pylint: disable=protected-access
 85            context._message_id = update.effective_message.message_id
 86
 87        # Remember to return the object
 88        return context
 89
 90
 91async def start(update: Update, context: CustomContext) -> None:
 92    """Display a message with a button."""
 93    await update.message.reply_html(
 94        "This button was clicked <i>0</i> times.",
 95        reply_markup=InlineKeyboardMarkup.from_button(
 96            InlineKeyboardButton(text="Click me!", callback_data="button")
 97        ),
 98    )
 99
100
101async def count_click(update: Update, context: CustomContext) -> None:
102    """Update the click count for the message."""
103    context.message_clicks += 1
104    await update.callback_query.answer()
105    await update.effective_message.edit_text(
106        f"This button was clicked <i>{context.message_clicks}</i> times.",
107        reply_markup=InlineKeyboardMarkup.from_button(
108            InlineKeyboardButton(text="Click me!", callback_data="button")
109        ),
110        parse_mode=ParseMode.HTML,
111    )
112
113
114async def print_users(update: Update, context: CustomContext) -> None:
115    """Show which users have been using this bot."""
116    await update.message.reply_text(
117        f"The following user IDs have used this bot: {', '.join(map(str, context.bot_user_ids))}"
118    )
119
120
121async def track_users(update: Update, context: CustomContext) -> None:
122    """Store the user id of the incoming update, if any."""
123    if update.effective_user:
124        context.bot_user_ids.add(update.effective_user.id)
125
126
127def main() -> None:
128    """Run the bot."""
129    context_types = ContextTypes(context=CustomContext, chat_data=ChatData)
130    application = Application.builder().token("TOKEN").context_types(context_types).build()
131
132    # run track_users in its own group to not interfere with the user handlers
133    application.add_handler(TypeHandler(Update, track_users), group=-1)
134    application.add_handler(CommandHandler("start", start))
135    application.add_handler(CallbackQueryHandler(count_click))
136    application.add_handler(CommandHandler("print_users", print_users))
137
138    application.run_polling(allowed_updates=Update.ALL_TYPES)
139
140
141if __name__ == "__main__":
142    main()