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"""
16import asyncio
17import html
18import logging
19from dataclasses import dataclass
20from http import HTTPStatus
21
22import uvicorn
23from starlette.applications import Starlette
24from starlette.requests import Request
25from starlette.responses import PlainTextResponse, Response
26from starlette.routing import Route
27
28from telegram import Update
29from telegram.constants import ParseMode
30from telegram.ext import (
31 Application,
32 CallbackContext,
33 CommandHandler,
34 ContextTypes,
35 ExtBot,
36 TypeHandler,
37)
38
39# Enable logging
40logging.basicConfig(
41 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
42)
43# set higher logging level for httpx to avoid all GET and POST requests being logged
44logging.getLogger("httpx").setLevel(logging.WARNING)
45
46logger = logging.getLogger(__name__)
47
48# Define configuration constants
49URL = "https://domain.tld"
50ADMIN_CHAT_ID = 123456
51PORT = 8000
52TOKEN = "123:ABC" # nosec B105
53
54
55@dataclass
56class WebhookUpdate:
57 """Simple dataclass to wrap a custom update type"""
58
59 user_id: int
60 payload: str
61
62
63class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
64 """
65 Custom CallbackContext class that makes `user_data` available for updates of type
66 `WebhookUpdate`.
67 """
68
69 @classmethod
70 def from_update(
71 cls,
72 update: object,
73 application: "Application",
74 ) -> "CustomContext":
75 if isinstance(update, WebhookUpdate):
76 return cls(application=application, user_id=update.user_id)
77 return super().from_update(update, application)
78
79
80async def start(update: Update, context: CustomContext) -> None:
81 """Display a message with instructions on how to use this bot."""
82 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
83 text = (
84 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
85 f"To post a custom update, call <code>{payload_url}</code>."
86 )
87 await update.message.reply_html(text=text)
88
89
90async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
91 """Handle custom updates."""
92 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
93 payloads = context.user_data.setdefault("payloads", [])
94 payloads.append(update.payload)
95 combined_payloads = "</code>\n• <code>".join(payloads)
96 text = (
97 f"The user {chat_member.user.mention_html()} has sent a new payload. "
98 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
99 )
100 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
101
102
103async def main() -> None:
104 """Set up PTB application and a web application for handling the incoming requests."""
105 context_types = ContextTypes(context=CustomContext)
106 # Here we set updater to None because we want our custom webhook server to handle the updates
107 # and hence we don't need an Updater instance
108 application = (
109 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
110 )
111
112 # register handlers
113 application.add_handler(CommandHandler("start", start))
114 application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
115
116 # Pass webhook settings to telegram
117 await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
118
119 # Set up webserver
120 async def telegram(request: Request) -> Response:
121 """Handle incoming Telegram updates by putting them into the `update_queue`"""
122 await application.update_queue.put(
123 Update.de_json(data=await request.json(), bot=application.bot)
124 )
125 return Response()
126
127 async def custom_updates(request: Request) -> PlainTextResponse:
128 """
129 Handle incoming webhook updates by also putting them into the `update_queue` if
130 the required parameters were passed correctly.
131 """
132 try:
133 user_id = int(request.query_params["user_id"])
134 payload = request.query_params["payload"]
135 except KeyError:
136 return PlainTextResponse(
137 status_code=HTTPStatus.BAD_REQUEST,
138 content="Please pass both `user_id` and `payload` as query parameters.",
139 )
140 except ValueError:
141 return PlainTextResponse(
142 status_code=HTTPStatus.BAD_REQUEST,
143 content="The `user_id` must be a string!",
144 )
145
146 await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
147 return PlainTextResponse("Thank you for the submission! It's being forwarded.")
148
149 async def health(_: Request) -> PlainTextResponse:
150 """For the health endpoint, reply with a simple plain text message."""
151 return PlainTextResponse(content="The bot is still running fine :)")
152
153 starlette_app = Starlette(
154 routes=[
155 Route("/telegram", telegram, methods=["POST"]),
156 Route("/healthcheck", health, methods=["GET"]),
157 Route("/submitpayload", custom_updates, methods=["POST", "GET"]),
158 ]
159 )
160 webserver = uvicorn.Server(
161 config=uvicorn.Config(
162 app=starlette_app,
163 port=PORT,
164 use_colors=False,
165 host="127.0.0.1",
166 )
167 )
168
169 # Run application and webserver together
170 async with application:
171 await application.start()
172 await webserver.serve()
173 await application.stop()
174
175
176if __name__ == "__main__":
177 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"""
16import asyncio
17import html
18import logging
19from dataclasses import dataclass
20from http import HTTPStatus
21
22import uvicorn
23from asgiref.wsgi import WsgiToAsgi
24from flask import Flask, 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 flask_app = Flask(__name__)
119
120 @flask_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(Update.de_json(data=request.json, bot=application.bot))
124 return Response(status=HTTPStatus.OK)
125
126 @flask_app.route("/submitpayload", methods=["GET", "POST"]) # type: ignore[misc]
127 async def custom_updates() -> Response:
128 """
129 Handle incoming webhook updates by also putting them into the `update_queue` if
130 the required parameters were passed correctly.
131 """
132 try:
133 user_id = int(request.args["user_id"])
134 payload = request.args["payload"]
135 except KeyError:
136 abort(
137 HTTPStatus.BAD_REQUEST,
138 "Please pass both `user_id` and `payload` as query parameters.",
139 )
140 except ValueError:
141 abort(HTTPStatus.BAD_REQUEST, "The `user_id` must be a string!")
142
143 await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
144 return Response(status=HTTPStatus.OK)
145
146 @flask_app.get("/healthcheck") # type: ignore[misc]
147 async def health() -> Response:
148 """For the health endpoint, reply with a simple plain text message."""
149 response = make_response("The bot is still running fine :)", HTTPStatus.OK)
150 response.mimetype = "text/plain"
151 return response
152
153 webserver = uvicorn.Server(
154 config=uvicorn.Config(
155 app=WsgiToAsgi(flask_app),
156 port=PORT,
157 use_colors=False,
158 host="127.0.0.1",
159 )
160 )
161
162 # Run application and webserver together
163 async with application:
164 await application.start()
165 await webserver.serve()
166 await application.stop()
167
168
169if __name__ == "__main__":
170 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"""
16import asyncio
17import html
18import logging
19from dataclasses import dataclass
20from http import HTTPStatus
21
22import uvicorn
23from quart import Quart, Response, abort, make_response, request
24
25from telegram import Update
26from telegram.constants import ParseMode
27from telegram.ext import (
28 Application,
29 CallbackContext,
30 CommandHandler,
31 ContextTypes,
32 ExtBot,
33 TypeHandler,
34)
35
36# Enable logging
37logging.basicConfig(
38 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
39)
40# set higher logging level for httpx to avoid all GET and POST requests being logged
41logging.getLogger("httpx").setLevel(logging.WARNING)
42
43logger = logging.getLogger(__name__)
44
45# Define configuration constants
46URL = "https://domain.tld"
47ADMIN_CHAT_ID = 123456
48PORT = 8000
49TOKEN = "123:ABC" # nosec B105
50
51
52@dataclass
53class WebhookUpdate:
54 """Simple dataclass to wrap a custom update type"""
55
56 user_id: int
57 payload: str
58
59
60class CustomContext(CallbackContext[ExtBot, dict, dict, dict]):
61 """
62 Custom CallbackContext class that makes `user_data` available for updates of type
63 `WebhookUpdate`.
64 """
65
66 @classmethod
67 def from_update(
68 cls,
69 update: object,
70 application: "Application",
71 ) -> "CustomContext":
72 if isinstance(update, WebhookUpdate):
73 return cls(application=application, user_id=update.user_id)
74 return super().from_update(update, application)
75
76
77async def start(update: Update, context: CustomContext) -> None:
78 """Display a message with instructions on how to use this bot."""
79 payload_url = html.escape(f"{URL}/submitpayload?user_id=<your user id>&payload=<payload>")
80 text = (
81 f"To check if the bot is still running, call <code>{URL}/healthcheck</code>.\n\n"
82 f"To post a custom update, call <code>{payload_url}</code>."
83 )
84 await update.message.reply_html(text=text)
85
86
87async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
88 """Handle custom updates."""
89 chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id)
90 payloads = context.user_data.setdefault("payloads", [])
91 payloads.append(update.payload)
92 combined_payloads = "</code>\n• <code>".join(payloads)
93 text = (
94 f"The user {chat_member.user.mention_html()} has sent a new payload. "
95 f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>"
96 )
97 await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML)
98
99
100async def main() -> None:
101 """Set up PTB application and a web application for handling the incoming requests."""
102 context_types = ContextTypes(context=CustomContext)
103 # Here we set updater to None because we want our custom webhook server to handle the updates
104 # and hence we don't need an Updater instance
105 application = (
106 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
107 )
108
109 # register handlers
110 application.add_handler(CommandHandler("start", start))
111 application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
112
113 # Pass webhook settings to telegram
114 await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
115
116 # Set up webserver
117 quart_app = Quart(__name__)
118
119 @quart_app.post("/telegram") # type: ignore[misc]
120 async def telegram() -> Response:
121 """Handle incoming Telegram updates by putting them into the `update_queue`"""
122 await application.update_queue.put(
123 Update.de_json(data=await request.get_json(), bot=application.bot)
124 )
125 return Response(status=HTTPStatus.OK)
126
127 @quart_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 @quart_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 = await 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=quart_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 `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"""
16import asyncio
17import html
18import json
19import logging
20from dataclasses import dataclass
21from uuid import uuid4
22
23import uvicorn
24from django.conf import settings
25from django.core.asgi import get_asgi_application
26from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
27from django.urls import path
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 telegram(request: HttpRequest) -> HttpResponse:
105 """Handle incoming Telegram updates by putting them into the `update_queue`"""
106 await ptb_application.update_queue.put(
107 Update.de_json(data=json.loads(request.body), bot=ptb_application.bot)
108 )
109 return HttpResponse()
110
111
112async def custom_updates(request: HttpRequest) -> HttpResponse:
113 """
114 Handle incoming webhook updates by also putting them into the `update_queue` if
115 the required parameters were passed correctly.
116 """
117 try:
118 user_id = int(request.GET["user_id"])
119 payload = request.GET["payload"]
120 except KeyError:
121 return HttpResponseBadRequest(
122 "Please pass both `user_id` and `payload` as query parameters.",
123 )
124 except ValueError:
125 return HttpResponseBadRequest("The `user_id` must be a string!")
126
127 await ptb_application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
128 return HttpResponse()
129
130
131async def health(_: HttpRequest) -> HttpResponse:
132 """For the health endpoint, reply with a simple plain text message."""
133 return HttpResponse("The bot is still running fine :)")
134
135
136# Set up PTB application and a web application for handling the incoming requests.
137
138context_types = ContextTypes(context=CustomContext)
139# Here we set updater to None because we want our custom webhook server to handle the updates
140# and hence we don't need an Updater instance
141ptb_application = (
142 Application.builder().token(TOKEN).updater(None).context_types(context_types).build()
143)
144
145# register handlers
146ptb_application.add_handler(CommandHandler("start", start))
147ptb_application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))
148
149urlpatterns = [
150 path("telegram", telegram, name="Telegram updates"),
151 path("submitpayload", custom_updates, name="custom updates"),
152 path("healthcheck", health, name="health check"),
153]
154settings.configure(ROOT_URLCONF=__name__, SECRET_KEY=uuid4().hex)
155
156
157async def main() -> None:
158 """Finalize configuration and run the applications."""
159 webserver = uvicorn.Server(
160 config=uvicorn.Config(
161 app=get_asgi_application(),
162 port=PORT,
163 use_colors=False,
164 host="127.0.0.1",
165 )
166 )
167
168 # Pass webhook settings to telegram
169 await ptb_application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
170
171 # Run application and webserver together
172 async with ptb_application:
173 await ptb_application.start()
174 await webserver.serve()
175 await ptb_application.stop()
176
177
178if __name__ == "__main__":
179 asyncio.run(main())