customwebhookbot.py¶
This example is available for different web frameworks. You can select your preferred framework by opening one of the tabs above the code example.
Hint
The following examples show how different Python web frameworks can be used alongside PTB. This can be useful for two use cases:
For extending the functionality of your existing bot to handling updates of external services
For extending the functionality of your exisiting web application to also include chat bot functionality
How the PTB and web framework components of the examples below are viewed surely depends on which use case one has in mind. We are fully aware that a combination of PTB with web frameworks will always mean finding a tradeoff between usability and best practices for both PTB and the web framework and these examples are certainly far from optimal solutions. Please understand them as starting points and use your expertise of the web framework of your choosing to build up on them. You are of course also very welcome to help improve these examples!
1#!/usr/bin/env python
2# This program is dedicated to the public domain under the CC0 license.
3# pylint: disable=import-error,unused-argument
4"""
5Simple example of a bot that uses a custom webhook setup and handles custom updates.
6For the custom webhook setup, the libraries `starlette` and `uvicorn` are used. Please install
7them as `pip install starlette~=0.20.0 uvicorn~=0.23.2`.
8Note that any other `asyncio` based web server framework can be used for a custom webhook setup
9just as well.
10
11Usage:
12Set bot Token, URL, admin CHAT_ID and PORT after the imports.
13You may also need to change the `listen` value in the uvicorn configuration to match your setup.
14Press Ctrl-C on the command line or send a signal to the process to stop the bot.
15"""
16
17import asyncio
18import html
19import logging
20from dataclasses import dataclass
21from http import HTTPStatus
22
23import uvicorn
24from starlette.applications import Starlette
25from starlette.requests import Request
26from starlette.responses import PlainTextResponse, Response
27from starlette.routing import Route
28
29from telegram import Update
30from telegram.constants import ParseMode
31from telegram.ext import (
32 Application,
33 CallbackContext,
34 CommandHandler,
35 ContextTypes,
36 ExtBot,
37 TypeHandler,
38)
39
40# Enable logging
41logging.basicConfig(
42 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
43)
44# set higher logging level for httpx to avoid all GET and POST requests being logged
45logging.getLogger("httpx").setLevel(logging.WARNING)
46
47logger = logging.getLogger(__name__)
48
49# Define configuration constants
50URL = "https://domain.tld"
51ADMIN_CHAT_ID = 123456
52PORT = 8000
53TOKEN = "123:ABC" # nosec B105
54
55
56@dataclass
57class WebhookUpdate:
58 """Simple dataclass to wrap a custom update type"""
59
60 user_id: int
61 payload: str
62
63
64class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
65 """
66 Custom CallbackContext class that makes `user_data` available for updates of type
67 `WebhookUpdate`.
68 """
69
70 @classmethod
71 def from_update(
72 cls,
73 update: object,
74 application: "Application",
75 ) -> "CustomContext":
76 if isinstance(update, WebhookUpdate):
77 return cls(application=application, user_id=update.user_id)
78 return super().from_update(update, application)
79
80
81async def start(update: Update, context: CustomContext) -> None:
82 """Display a message with instructions on how to use this bot."""
83 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
84 text = (
85 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
86 f"To post a custom update, call <code>{payload_url}</code>."
87 )
88 await update.message.reply_html(text=text)
89
90
91async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
92 """Handle custom updates."""
93 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
94 payloads = context.user_data.setdefault("payloads", [])
95 payloads.append(update.payload)
96 combined_payloads = "</code>\n• <code>".join(payloads)
97 text = (
98 f"The user {chat_member.user.mention_html()} has sent a new payload. "
99 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
100 )
101 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
102
103
104async def main() -> None:
105 """Set up PTB application and a web application for handling the incoming requests."""
106 context_types = ContextTypes(context=CustomContext)
107 # Here we set updater to None because we want our custom webhook server to handle the updates
108 # and hence we don't need an Updater instance
109 application = (
110 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
111 )
112
113 # register handlers
114 application.add_handler(CommandHandler("start", start))
115 application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
116
117 # Pass webhook settings to telegram
118 await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
119
120 # Set up webserver
121 async def telegram(request: Request) -> Response:
122 """Handle incoming Telegram updates by putting them into the `update_queue`"""
123 await application.update_queue.put(
124 Update.de_json(data=await request.json(), bot=application.bot)
125 )
126 return Response()
127
128 async def custom_updates(request: Request) -> PlainTextResponse:
129 """
130 Handle incoming webhook updates by also putting them into the `update_queue` if
131 the required parameters were passed correctly.
132 """
133 try:
134 user_id = int(request.query_params["user_id"])
135 payload = request.query_params["payload"]
136 except KeyError:
137 return PlainTextResponse(
138 status_code=HTTPStatus.BAD_REQUEST,
139 content="Please pass both `user_id` and `payload` as query parameters.",
140 )
141 except ValueError:
142 return PlainTextResponse(
143 status_code=HTTPStatus.BAD_REQUEST,
144 content="The `user_id` must be a string!",
145 )
146
147 await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
148 return PlainTextResponse("Thank you for the submission! It's being forwarded.")
149
150 async def health(_: Request) -> PlainTextResponse:
151 """For the health endpoint, reply with a simple plain text message."""
152 return PlainTextResponse(content="The bot is still running fine :)")
153
154 starlette_app = Starlette(
155 routes=[
156 Route("/telegram", telegram, methods=["POST"]),
157 Route("/healthcheck", health, methods=["GET"]),
158 Route("/submitpayload", custom_updates, methods=["POST", "GET"]),
159 ]
160 )
161 webserver = uvicorn.Server(
162 config=uvicorn.Config(
163 app=starlette_app,
164 port=PORT,
165 use_colors=False,
166 host="127.0.0.1",
167 )
168 )
169
170 # Run application and webserver together
171 async with application:
172 await application.start()
173 await webserver.serve()
174 await application.stop()
175
176
177if __name__ == "__main__":
178 asyncio.run(main())
1#!/usr/bin/env python
2# This program is dedicated to the public domain under the CC0 license.
3# pylint: disable=import-error,unused-argument
4"""
5Simple example of a bot that uses a custom webhook setup and handles custom updates.
6For the custom webhook setup, the libraries `flask`, `asgiref` and `uvicorn` are used. Please
7install them as `pip install flask[async]~=2.3.2 uvicorn~=0.23.2 asgiref~=3.7.2`.
8Note that any other `asyncio` based web server framework can be used for a custom webhook setup
9just as well.
10
11Usage:
12Set bot Token, URL, admin CHAT_ID and PORT after the imports.
13You may also need to change the `listen` value in the uvicorn configuration to match your setup.
14Press Ctrl-C on the command line or send a signal to the process to stop the bot.
15"""
16
17import asyncio
18import html
19import logging
20from dataclasses import dataclass
21from http import HTTPStatus
22
23import uvicorn
24from asgiref.wsgi import WsgiToAsgi
25from flask import Flask, Response, abort, make_response, request
26
27from telegram import Update
28from telegram.constants import ParseMode
29from telegram.ext import (
30 Application,
31 CallbackContext,
32 CommandHandler,
33 ContextTypes,
34 ExtBot,
35 TypeHandler,
36)
37
38# Enable logging
39logging.basicConfig(
40 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
41)
42# set higher logging level for httpx to avoid all GET and POST requests being logged
43logging.getLogger("httpx").setLevel(logging.WARNING)
44
45logger = logging.getLogger(__name__)
46
47# Define configuration constants
48URL = "https://domain.tld"
49ADMIN_CHAT_ID = 123456
50PORT = 8000
51TOKEN = "123:ABC" # nosec B105
52
53
54@dataclass
55class WebhookUpdate:
56 """Simple dataclass to wrap a custom update type"""
57
58 user_id: int
59 payload: str
60
61
62class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
63 """
64 Custom CallbackContext class that makes `user_data` available for updates of type
65 `WebhookUpdate`.
66 """
67
68 @classmethod
69 def from_update(
70 cls,
71 update: object,
72 application: "Application",
73 ) -> "CustomContext":
74 if isinstance(update, WebhookUpdate):
75 return cls(application=application, user_id=update.user_id)
76 return super().from_update(update, application)
77
78
79async def start(update: Update, context: CustomContext) -> None:
80 """Display a message with instructions on how to use this bot."""
81 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
82 text = (
83 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
84 f"To post a custom update, call <code>{payload_url}</code>."
85 )
86 await update.message.reply_html(text=text)
87
88
89async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
90 """Handle custom updates."""
91 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
92 payloads = context.user_data.setdefault("payloads", [])
93 payloads.append(update.payload)
94 combined_payloads = "</code>\n• <code>".join(payloads)
95 text = (
96 f"The user {chat_member.user.mention_html()} has sent a new payload. "
97 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
98 )
99 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
100
101
102async def main() -> None:
103 """Set up PTB application and a web application for handling the incoming requests."""
104 context_types = ContextTypes(context=CustomContext)
105 # Here we set updater to None because we want our custom webhook server to handle the updates
106 # and hence we don't need an Updater instance
107 application = (
108 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
109 )
110
111 # register handlers
112 application.add_handler(CommandHandler("start", start))
113 application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
114
115 # Pass webhook settings to telegram
116 await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
117
118 # Set up webserver
119 flask_app = Flask(__name__)
120
121 @flask_app.post("/telegram") # type: ignore[misc]
122 async def telegram() -> Response:
123 """Handle incoming Telegram updates by putting them into the `update_queue`"""
124 await application.update_queue.put(Update.de_json(data=request.json, bot=application.bot))
125 return Response(status=HTTPStatus.OK)
126
127 @flask_app.route("/submitpayload", methods=["GET", "POST"]) # type: ignore[misc]
128 async def custom_updates() -> Response:
129 """
130 Handle incoming webhook updates by also putting them into the `update_queue` if
131 the required parameters were passed correctly.
132 """
133 try:
134 user_id = int(request.args["user_id"])
135 payload = request.args["payload"]
136 except KeyError:
137 abort(
138 HTTPStatus.BAD_REQUEST,
139 "Please pass both `user_id` and `payload` as query parameters.",
140 )
141 except ValueError:
142 abort(HTTPStatus.BAD_REQUEST, "The `user_id` must be a string!")
143
144 await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
145 return Response(status=HTTPStatus.OK)
146
147 @flask_app.get("/healthcheck") # type: ignore[misc]
148 async def health() -> Response:
149 """For the health endpoint, reply with a simple plain text message."""
150 response = make_response("The bot is still running fine :)", HTTPStatus.OK)
151 response.mimetype = "text/plain"
152 return response
153
154 webserver = uvicorn.Server(
155 config=uvicorn.Config(
156 app=WsgiToAsgi(flask_app),
157 port=PORT,
158 use_colors=False,
159 host="127.0.0.1",
160 )
161 )
162
163 # Run application and webserver together
164 async with application:
165 await application.start()
166 await webserver.serve()
167 await application.stop()
168
169
170if __name__ == "__main__":
171 asyncio.run(main())
1#!/usr/bin/env python
2# This program is dedicated to the public domain under the CC0 license.
3# pylint: disable=import-error,unused-argument
4"""
5Simple example of a bot that uses a custom webhook setup and handles custom updates.
6For the custom webhook setup, the libraries `quart` and `uvicorn` are used. Please
7install them as `pip install quart~=0.18.4 uvicorn~=0.23.2`.
8Note that any other `asyncio` based web server framework can be used for a custom webhook setup
9just as well.
10
11Usage:
12Set bot Token, URL, admin CHAT_ID and PORT after the imports.
13You may also need to change the `listen` value in the uvicorn configuration to match your setup.
14Press Ctrl-C on the command line or send a signal to the process to stop the bot.
15"""
16
17import asyncio
18import html
19import logging
20from dataclasses import dataclass
21from http import HTTPStatus
22
23import uvicorn
24from quart import Quart, Response, abort, make_response, request
25
26from telegram import Update
27from telegram.constants import ParseMode
28from telegram.ext import (
29 Application,
30 CallbackContext,
31 CommandHandler,
32 ContextTypes,
33 ExtBot,
34 TypeHandler,
35)
36
37# Enable logging
38logging.basicConfig(
39 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
40)
41# set higher logging level for httpx to avoid all GET and POST requests being logged
42logging.getLogger("httpx").setLevel(logging.WARNING)
43
44logger = logging.getLogger(__name__)
45
46# Define configuration constants
47URL = "https://domain.tld"
48ADMIN_CHAT_ID = 123456
49PORT = 8000
50TOKEN = "123:ABC" # nosec B105
51
52
53@dataclass
54class WebhookUpdate:
55 """Simple dataclass to wrap a custom update type"""
56
57 user_id: int
58 payload: str
59
60
61class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
62 """
63 Custom CallbackContext class that makes `user_data` available for updates of type
64 `WebhookUpdate`.
65 """
66
67 @classmethod
68 def from_update(
69 cls,
70 update: object,
71 application: "Application",
72 ) -> "CustomContext":
73 if isinstance(update, WebhookUpdate):
74 return cls(application=application, user_id=update.user_id)
75 return super().from_update(update, application)
76
77
78async def start(update: Update, context: CustomContext) -> None:
79 """Display a message with instructions on how to use this bot."""
80 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
81 text = (
82 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
83 f"To post a custom update, call <code>{payload_url}</code>."
84 )
85 await update.message.reply_html(text=text)
86
87
88async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
89 """Handle custom updates."""
90 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
91 payloads = context.user_data.setdefault("payloads", [])
92 payloads.append(update.payload)
93 combined_payloads = "</code>\n• <code>".join(payloads)
94 text = (
95 f"The user {chat_member.user.mention_html()} has sent a new payload. "
96 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
97 )
98 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
99
100
101async def main() -> None:
102 """Set up PTB application and a web application for handling the incoming requests."""
103 context_types = ContextTypes(context=CustomContext)
104 # Here we set updater to None because we want our custom webhook server to handle the updates
105 # and hence we don't need an Updater instance
106 application = (
107 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
108 )
109
110 # register handlers
111 application.add_handler(CommandHandler("start", start))
112 application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
113
114 # Pass webhook settings to telegram
115 await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
116
117 # Set up webserver
118 quart_app = Quart(__name__)
119
120 @quart_app.post("/telegram") # type: ignore[misc]
121 async def telegram() -> Response:
122 """Handle incoming Telegram updates by putting them into the `update_queue`"""
123 await application.update_queue.put(
124 Update.de_json(data=await request.get_json(), bot=application.bot)
125 )
126 return Response(status=HTTPStatus.OK)
127
128 @quart_app.route("/submitpayload", methods=["GET", "POST"]) # type: ignore[misc]
129 async def custom_updates() -> Response:
130 """
131 Handle incoming webhook updates by also putting them into the `update_queue` if
132 the required parameters were passed correctly.
133 """
134 try:
135 user_id = int(request.args["user_id"])
136 payload = request.args["payload"]
137 except KeyError:
138 abort(
139 HTTPStatus.BAD_REQUEST,
140 "Please pass both `user_id` and `payload` as query parameters.",
141 )
142 except ValueError:
143 abort(HTTPStatus.BAD_REQUEST, "The `user_id` must be a string!")
144
145 await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
146 return Response(status=HTTPStatus.OK)
147
148 @quart_app.get("/healthcheck") # type: ignore[misc]
149 async def health() -> Response:
150 """For the health endpoint, reply with a simple plain text message."""
151 response = await make_response("The bot is still running fine :)", HTTPStatus.OK)
152 response.mimetype = "text/plain"
153 return response
154
155 webserver = uvicorn.Server(
156 config=uvicorn.Config(
157 app=quart_app,
158 port=PORT,
159 use_colors=False,
160 host="127.0.0.1",
161 )
162 )
163
164 # Run application and webserver together
165 async with application:
166 await application.start()
167 await webserver.serve()
168 await application.stop()
169
170
171if __name__ == "__main__":
172 asyncio.run(main())
1#!/usr/bin/env python
2# This program is dedicated to the public domain under the CC0 license.
3# pylint: disable=import-error,unused-argument
4"""
5Simple example of a bot that uses a custom webhook setup and handles custom updates.
6For the custom webhook setup, the libraries `Django` and `uvicorn` are used. Please
7install them as `pip install Django~=4.2.4 uvicorn~=0.23.2`.
8Note that any other `asyncio` based web server framework can be used for a custom webhook setup
9just as well.
10
11Usage:
12Set bot Token, URL, admin CHAT_ID and PORT after the imports.
13You may also need to change the `listen` value in the uvicorn configuration to match your setup.
14Press Ctrl-C on the command line or send a signal to the process to stop the bot.
15"""
16
17import asyncio
18import html
19import json
20import logging
21from dataclasses import dataclass
22from uuid import uuid4
23
24import uvicorn
25from django.conf import settings
26from django.core.asgi import get_asgi_application
27from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
28from django.urls import path
29
30from telegram import Update
31from telegram.constants import ParseMode
32from telegram.ext import (
33 Application,
34 CallbackContext,
35 CommandHandler,
36 ContextTypes,
37 ExtBot,
38 TypeHandler,
39)
40
41# Enable logging
42logging.basicConfig(
43 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
44)
45# set higher logging level for httpx to avoid all GET and POST requests being logged
46logging.getLogger("httpx").setLevel(logging.WARNING)
47
48logger = logging.getLogger(__name__)
49
50# Define configuration constants
51URL = "https://domain.tld"
52ADMIN_CHAT_ID = 123456
53PORT = 8000
54TOKEN = "123:ABC" # nosec B105
55
56
57@dataclass
58class WebhookUpdate:
59 """Simple dataclass to wrap a custom update type"""
60
61 user_id: int
62 payload: str
63
64
65class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
66 """
67 Custom CallbackContext class that makes `user_data` available for updates of type
68 `WebhookUpdate`.
69 """
70
71 @classmethod
72 def from_update(
73 cls,
74 update: object,
75 application: "Application",
76 ) -> "CustomContext":
77 if isinstance(update, WebhookUpdate):
78 return cls(application=application, user_id=update.user_id)
79 return super().from_update(update, application)
80
81
82async def start(update: Update, context: CustomContext) -> None:
83 """Display a message with instructions on how to use this bot."""
84 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
85 text = (
86 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
87 f"To post a custom update, call <code>{payload_url}</code>."
88 )
89 await update.message.reply_html(text=text)
90
91
92async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
93 """Handle custom updates."""
94 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
95 payloads = context.user_data.setdefault("payloads", [])
96 payloads.append(update.payload)
97 combined_payloads = "</code>\n• <code>".join(payloads)
98 text = (
99 f"The user {chat_member.user.mention_html()} has sent a new payload. "
100 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
101 )
102 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
103
104
105async def telegram(request: HttpRequest) -> HttpResponse:
106 """Handle incoming Telegram updates by putting them into the `update_queue`"""
107 await ptb_application.update_queue.put(
108 Update.de_json(data=json.loads(request.body), bot=ptb_application.bot)
109 )
110 return HttpResponse()
111
112
113async def custom_updates(request: HttpRequest) -> HttpResponse:
114 """
115 Handle incoming webhook updates by also putting them into the `update_queue` if
116 the required parameters were passed correctly.
117 """
118 try:
119 user_id = int(request.GET["user_id"])
120 payload = request.GET["payload"]
121 except KeyError:
122 return HttpResponseBadRequest(
123 "Please pass both `user_id` and `payload` as query parameters.",
124 )
125 except ValueError:
126 return HttpResponseBadRequest("The `user_id` must be a string!")
127
128 await ptb_application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
129 return HttpResponse()
130
131
132async def health(_: HttpRequest) -> HttpResponse:
133 """For the health endpoint, reply with a simple plain text message."""
134 return HttpResponse("The bot is still running fine :)")
135
136
137# Set up PTB application and a web application for handling the incoming requests.
138
139context_types = ContextTypes(context=CustomContext)
140# Here we set updater to None because we want our custom webhook server to handle the updates
141# and hence we don't need an Updater instance
142ptb_application = (
143 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
144)
145
146# register handlers
147ptb_application.add_handler(CommandHandler("start", start))
148ptb_application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
149
150urlpatterns = [
151 path("telegram", telegram, name="Telegram updates"),
152 path("submitpayload", custom_updates, name="custom updates"),
153 path("healthcheck", health, name="health check"),
154]
155settings.configure(ROOT_URLCONF=__name__, SECRET_KEY=uuid4().hex)
156
157
158async def main() -> None:
159 """Finalize configuration and run the applications."""
160 webserver = uvicorn.Server(
161 config=uvicorn.Config(
162 app=get_asgi_application(),
163 port=PORT,
164 use_colors=False,
165 host="127.0.0.1",
166 )
167 )
168
169 # Pass webhook settings to telegram
170 await ptb_application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
171
172 # Run application and webserver together
173 async with ptb_application:
174 await ptb_application.start()
175 await webserver.serve()
176 await ptb_application.stop()
177
178
179if __name__ == "__main__":
180 asyncio.run(main())