mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-11-21 22:56:38 +01:00
Add Arguments chat/user_id
to CallbackContext
And Example On Custom Webhook Setups (#3059)
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
This commit is contained in:
parent
5f547f3725
commit
42276338b1
5 changed files with 248 additions and 62 deletions
|
@ -61,6 +61,9 @@ Uses the [`iro.js`](https://iro.js.org) JavaScript library to showcase a user in
|
||||||
### [`contexttypesbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/contexttypesbot.py)
|
### [`contexttypesbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/contexttypesbot.py)
|
||||||
This example showcases how `telegram.ext.ContextTypes` can be used to customize the `context` argument of handler and job callbacks.
|
This example showcases how `telegram.ext.ContextTypes` can be used to customize the `context` argument of handler and job callbacks.
|
||||||
|
|
||||||
|
### [`customwebhookbot.py`](customwebhookbot.py)
|
||||||
|
This example showcases how a custom webhook setup can be used in combination with `telegram.ext.Application`.
|
||||||
|
|
||||||
### [`arbitrarycallbackdatabot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/arbitrarycallbackdatabot.py)
|
### [`arbitrarycallbackdatabot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/arbitrarycallbackdatabot.py)
|
||||||
This example showcases how PTBs "arbitrary callback data" feature can be used.
|
This example showcases how PTBs "arbitrary callback data" feature can be used.
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ class ChatData:
|
||||||
class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
|
class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
|
||||||
"""Custom class for context."""
|
"""Custom class for context."""
|
||||||
|
|
||||||
def __init__(self, application: Application):
|
def __init__(self, application: Application, chat_id: int = None, user_id: int = None):
|
||||||
super().__init__(application=application)
|
super().__init__(application=application, chat_id=chat_id, user_id=user_id)
|
||||||
self._message_id: Optional[int] = None
|
self._message_id: Optional[int] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
192
examples/customwebhookbot.py
Normal file
192
examples/customwebhookbot.py
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# This program is dedicated to the public domain under the CC0 license.
|
||||||
|
# pylint: disable=import-error,wrong-import-position
|
||||||
|
"""
|
||||||
|
Simple example of a bot that uses a custom webhook setup and handles custom updates.
|
||||||
|
For the custom webhook setup, the libraries `starlette` and `uvicorn` are used. Please install
|
||||||
|
them as `pip install starlette~=0.20.0 uvicorn~=0.17.0`.
|
||||||
|
Note that any other `asyncio` based web server framework can be used for a custom webhook setup
|
||||||
|
just as well.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Set bot token, url, admin chat_id and port at the start of the `main` function.
|
||||||
|
You may also need to change the `listen` value in the uvicorn configuration to match your setup.
|
||||||
|
Press Ctrl-C on the command line or send a signal to the process to stop the bot.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import html
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import PlainTextResponse, Response
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
|
from telegram import __version__ as TG_VER
|
||||||
|
|
||||||
|
try:
|
||||||
|
from telegram import __version_info__
|
||||||
|
except ImportError:
|
||||||
|
__version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment]
|
||||||
|
|
||||||
|
if __version_info__ < (20, 0, 0, "alpha", 1):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"This example is not compatible with your current PTB version {TG_VER}. To view the "
|
||||||
|
f"{TG_VER} version of this example, "
|
||||||
|
f"visit https://github.com/python-telegram-bot/python-telegram-bot/tree/v{TG_VER}/examples"
|
||||||
|
)
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.constants import ParseMode
|
||||||
|
from telegram.ext import (
|
||||||
|
Application,
|
||||||
|
CallbackContext,
|
||||||
|
CommandHandler,
|
||||||
|
ContextTypes,
|
||||||
|
ExtBot,
|
||||||
|
TypeHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WebhookUpdate:
|
||||||
|
"""Simple dataclass to wrap a custom update type"""
|
||||||
|
|
||||||
|
user_id: int
|
||||||
|
payload: str
|
||||||
|
|
||||||
|
|
||||||
|
class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
|
||||||
|
"""
|
||||||
|
Custom CallbackContext class that makes `user_data` available for updates of type
|
||||||
|
`WebhookUpdate`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_update(
|
||||||
|
cls,
|
||||||
|
update: object,
|
||||||
|
application: "Application",
|
||||||
|
) -> "CustomContext":
|
||||||
|
if isinstance(update, WebhookUpdate):
|
||||||
|
return cls(application=application, user_id=update.user_id)
|
||||||
|
return super().from_update(update, application)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update, context: CustomContext) -> None:
|
||||||
|
"""Display a message with instructions on how to use this bot."""
|
||||||
|
url = context.bot_data["url"]
|
||||||
|
payload_url = html.escape(f"{url}/submitpayload?user_id=<your user id>&payload=<payload>")
|
||||||
|
text = (
|
||||||
|
f"To check if the bot is still running, call <code>{url}/healthcheck</code>.\n\n"
|
||||||
|
f"To post a custom update, call <code>{payload_url}</code>."
|
||||||
|
)
|
||||||
|
await update.message.reply_html(text=text)
|
||||||
|
|
||||||
|
|
||||||
|
async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
|
||||||
|
"""Callback that handles the custom updates."""
|
||||||
|
chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
|
||||||
|
payloads = context.user_data.setdefault("payloads", [])
|
||||||
|
payloads.append(update.payload)
|
||||||
|
combined_payloads = "</code>\n• <code>".join(payloads)
|
||||||
|
text = (
|
||||||
|
f"The user {chat_member.user.mention_html()} has sent a new payload. "
|
||||||
|
f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
|
||||||
|
)
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=context.bot_data["admin_chat_id"], text=text, parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Set up the application and a custom webserver."""
|
||||||
|
url = "https://domain.tld"
|
||||||
|
admin_chat_id = 123456
|
||||||
|
port = 8000
|
||||||
|
|
||||||
|
context_types = ContextTypes(context=CustomContext)
|
||||||
|
# Here we set updater to None because we want our custom webhook server to handle the updates
|
||||||
|
# and hence we don't need an Updater instance
|
||||||
|
application = (
|
||||||
|
Application.builder().token("TOKEN").updater(None).context_types(context_types).build()
|
||||||
|
)
|
||||||
|
# save the values in `bot_data` such that we may easily access them in the callbacks
|
||||||
|
application.bot_data["url"] = url
|
||||||
|
application.bot_data["admin_chat_id"] = admin_chat_id
|
||||||
|
|
||||||
|
# register handlers
|
||||||
|
application.add_handler(CommandHandler("start", start))
|
||||||
|
application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
|
||||||
|
|
||||||
|
# Pass webhook settings to telegram
|
||||||
|
await application.bot.set_webhook(url=f"{url}/telegram")
|
||||||
|
|
||||||
|
# Set up webserver
|
||||||
|
async def telegram(request: Request) -> Response:
|
||||||
|
"""Handle incoming Telegram updates by putting them into the `update_queue`"""
|
||||||
|
await application.update_queue.put(
|
||||||
|
Update.de_json(data=await request.json(), bot=application.bot)
|
||||||
|
)
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
async def custom_updates(request: Request) -> PlainTextResponse:
|
||||||
|
"""
|
||||||
|
Handle incoming webhook updates by also putting them into the `update_queue` if
|
||||||
|
the required parameters were passed correctly.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_id = int(request.query_params["user_id"])
|
||||||
|
payload = request.query_params["payload"]
|
||||||
|
except KeyError:
|
||||||
|
return PlainTextResponse(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
content="Please pass both `user_id` and `payload` as query parameters.",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return PlainTextResponse(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
content="The `user_id` must be a string!",
|
||||||
|
)
|
||||||
|
|
||||||
|
await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
|
||||||
|
return PlainTextResponse("Thank you for the submission! It's being forwarded.")
|
||||||
|
|
||||||
|
async def health(_: Request) -> PlainTextResponse:
|
||||||
|
"""For the health endpoint, reply with a simple plain text message."""
|
||||||
|
return PlainTextResponse(content="The bot is still running fine :)")
|
||||||
|
|
||||||
|
starlette_app = Starlette(
|
||||||
|
routes=[
|
||||||
|
Route("/telegram", telegram, methods=["POST"]),
|
||||||
|
Route("/healthcheck", health, methods=["GET"]),
|
||||||
|
Route("/submitpayload", custom_updates, methods=["POST", "GET"]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
webserver = uvicorn.Server(
|
||||||
|
config=uvicorn.Config(
|
||||||
|
app=starlette_app,
|
||||||
|
port=port,
|
||||||
|
use_colors=False,
|
||||||
|
host="127.0.0.1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run application and webserver together
|
||||||
|
async with application:
|
||||||
|
await application.start()
|
||||||
|
await webserver.serve()
|
||||||
|
await application.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
10
setup.cfg
10
setup.cfg
|
@ -61,9 +61,6 @@ disallow_incomplete_defs = True
|
||||||
disallow_untyped_decorators = True
|
disallow_untyped_decorators = True
|
||||||
show_error_codes = True
|
show_error_codes = True
|
||||||
|
|
||||||
[mypy-telegram.vendor.*]
|
|
||||||
ignore_errors = True
|
|
||||||
|
|
||||||
# For some files, it's easier to just disable strict-optional all together instead of
|
# For some files, it's easier to just disable strict-optional all together instead of
|
||||||
# cluttering the code with `# type: ignore`s or stuff like
|
# cluttering the code with `# type: ignore`s or stuff like
|
||||||
# `if self.text is None: raise RuntimeError()`
|
# `if self.text is None: raise RuntimeError()`
|
||||||
|
@ -76,3 +73,10 @@ warn_unused_ignores = False
|
||||||
|
|
||||||
[mypy-apscheduler.*]
|
[mypy-apscheduler.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
# uvicorn and starlette are only used for the `customwebhookbot.py` example
|
||||||
|
# let's just ignore type checking for them for now
|
||||||
|
[mypy-uvicorn.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
[mypy-starlette.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
|
@ -18,18 +18,7 @@
|
||||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
"""This module contains the CallbackContext class."""
|
"""This module contains the CallbackContext class."""
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Coroutine, Dict, Generic, List, Match, NoReturn, Optional, Type
|
||||||
TYPE_CHECKING,
|
|
||||||
Coroutine,
|
|
||||||
Dict,
|
|
||||||
Generic,
|
|
||||||
List,
|
|
||||||
Match,
|
|
||||||
NoReturn,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
)
|
|
||||||
|
|
||||||
from telegram._callbackquery import CallbackQuery
|
from telegram._callbackquery import CallbackQuery
|
||||||
from telegram._update import Update
|
from telegram._update import Update
|
||||||
|
@ -83,6 +72,14 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
Args:
|
Args:
|
||||||
application (:class:`telegram.ext.Application`): The application associated with this
|
application (:class:`telegram.ext.Application`): The application associated with this
|
||||||
context.
|
context.
|
||||||
|
chat_id (:obj:`int`, optional): The ID of the chat associated with this object. Used
|
||||||
|
to provide :attr:`chat_data`.
|
||||||
|
|
||||||
|
.. versionadded:: 20.0
|
||||||
|
user_id (:obj:`int`, optional): The ID of the user associated with this object. Used
|
||||||
|
to provide :attr:`user_data`.
|
||||||
|
|
||||||
|
.. versionadded:: 20.0
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
coroutine (:term:`coroutine function`): Optional. Only present in error handlers if the
|
coroutine (:term:`coroutine function`): Optional. Only present in error handlers if the
|
||||||
|
@ -109,8 +106,8 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_application",
|
"_application",
|
||||||
"_chat_id_and_data",
|
"_chat_id",
|
||||||
"_user_id_and_data",
|
"_user_id",
|
||||||
"args",
|
"args",
|
||||||
"matches",
|
"matches",
|
||||||
"error",
|
"error",
|
||||||
|
@ -119,10 +116,15 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
"__dict__",
|
"__dict__",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self: "CCT", application: "Application[BT, CCT, UD, CD, BD, Any]"):
|
def __init__(
|
||||||
|
self: "CCT",
|
||||||
|
application: "Application[BT, CCT, UD, CD, BD, Any]",
|
||||||
|
chat_id: int = None,
|
||||||
|
user_id: int = None,
|
||||||
|
):
|
||||||
self._application = application
|
self._application = application
|
||||||
self._chat_id_and_data: Optional[Tuple[int, CD]] = None
|
self._chat_id = chat_id
|
||||||
self._user_id_and_data: Optional[Tuple[int, UD]] = None
|
self._user_id = user_id
|
||||||
self.args: Optional[List[str]] = None
|
self.args: Optional[List[str]] = None
|
||||||
self.matches: Optional[List[Match]] = None
|
self.matches: Optional[List[Match]] = None
|
||||||
self.error: Optional[Exception] = None
|
self.error: Optional[Exception] = None
|
||||||
|
@ -159,8 +161,8 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
|
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
|
||||||
Storing-bot,-user-and-chat-related-data#chat-migration>`_.
|
Storing-bot,-user-and-chat-related-data#chat-migration>`_.
|
||||||
"""
|
"""
|
||||||
if self._chat_id_and_data:
|
if self._chat_id is not None:
|
||||||
return self._chat_id_and_data[1]
|
return self._application.chat_data[self._chat_id]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@chat_data.setter
|
@chat_data.setter
|
||||||
|
@ -175,8 +177,8 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
For each update from the same user it will be the same :obj:`ContextTypes.user_data`.
|
For each update from the same user it will be the same :obj:`ContextTypes.user_data`.
|
||||||
Defaults to :obj:`dict`.
|
Defaults to :obj:`dict`.
|
||||||
"""
|
"""
|
||||||
if self._user_id_and_data:
|
if self._user_id is not None:
|
||||||
return self._user_id_and_data[1]
|
return self._application.user_data[self._user_id]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@user_data.setter
|
@user_data.setter
|
||||||
|
@ -200,16 +202,14 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
if self.application.persistence:
|
if self.application.persistence:
|
||||||
if self.application.persistence.store_data.bot_data:
|
if self.application.persistence.store_data.bot_data:
|
||||||
await self.application.persistence.refresh_bot_data(self.bot_data)
|
await self.application.persistence.refresh_bot_data(self.bot_data)
|
||||||
if (
|
if self.application.persistence.store_data.chat_data and self._chat_id is not None:
|
||||||
self.application.persistence.store_data.chat_data
|
await self.application.persistence.refresh_chat_data(
|
||||||
and self._chat_id_and_data is not None
|
chat_id=self._chat_id, chat_data=self.chat_data
|
||||||
):
|
)
|
||||||
await self.application.persistence.refresh_chat_data(*self._chat_id_and_data)
|
if self.application.persistence.store_data.user_data and self._user_id is not None:
|
||||||
if (
|
await self.application.persistence.refresh_user_data(
|
||||||
self.application.persistence.store_data.user_data
|
user_id=self._user_id, user_data=self.user_data
|
||||||
and self._user_id_and_data is not None
|
)
|
||||||
):
|
|
||||||
await self.application.persistence.refresh_user_data(*self._user_id_and_data)
|
|
||||||
|
|
||||||
def drop_callback_data(self, callback_query: CallbackQuery) -> None:
|
def drop_callback_data(self, callback_query: CallbackQuery) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -264,6 +264,12 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
context.
|
context.
|
||||||
job (:class:`telegram.ext.Job`, optional): The job associated with the error.
|
job (:class:`telegram.ext.Job`, optional): The job associated with the error.
|
||||||
|
|
||||||
|
.. versionadded:: 20.0
|
||||||
|
coroutine (:term:`coroutine function`, optional): The coroutine function associated
|
||||||
|
with this error if the error was caused by a coroutine run with
|
||||||
|
:meth:`Application.create_task` or a handler callback with
|
||||||
|
:attr:`block=False <BaseHandler.block>`.
|
||||||
|
|
||||||
.. versionadded:: 20.0
|
.. versionadded:: 20.0
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -295,23 +301,15 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
Returns:
|
Returns:
|
||||||
:class:`telegram.ext.CallbackContext`
|
:class:`telegram.ext.CallbackContext`
|
||||||
"""
|
"""
|
||||||
self = cls(application) # type: ignore
|
if isinstance(update, Update):
|
||||||
|
|
||||||
if update is not None and isinstance(update, Update):
|
|
||||||
chat = update.effective_chat
|
chat = update.effective_chat
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
|
||||||
if chat:
|
chat_id = chat.id if chat else None
|
||||||
self._chat_id_and_data = (
|
user_id = user.id if user else None
|
||||||
chat.id,
|
|
||||||
application.chat_data[chat.id],
|
return cls(application, chat_id=chat_id, user_id=user_id) # type: ignore
|
||||||
)
|
return cls(application) # type: ignore
|
||||||
if user:
|
|
||||||
self._user_id_and_data = (
|
|
||||||
user.id,
|
|
||||||
application.user_data[user.id],
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_job(
|
def from_job(
|
||||||
|
@ -333,19 +331,8 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||||
Returns:
|
Returns:
|
||||||
:class:`telegram.ext.CallbackContext`
|
:class:`telegram.ext.CallbackContext`
|
||||||
"""
|
"""
|
||||||
self = cls(application) # type: ignore
|
self = cls(application, chat_id=job.chat_id, user_id=job.user_id) # type: ignore
|
||||||
self.job = job
|
self.job = job
|
||||||
|
|
||||||
if job.chat_id:
|
|
||||||
self._chat_id_and_data = (
|
|
||||||
job.chat_id,
|
|
||||||
application.chat_data[job.chat_id],
|
|
||||||
)
|
|
||||||
if job.user_id:
|
|
||||||
self._user_id_and_data = (
|
|
||||||
job.user_id,
|
|
||||||
application.user_data[job.user_id],
|
|
||||||
)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def update(self, data: Dict[str, object]) -> None:
|
def update(self, data: Dict[str, object]) -> None:
|
||||||
|
|
Loading…
Reference in a new issue