diff options
-rw-r--r-- | bot.py | 40 | ||||
-rw-r--r-- | callback.py | 16 | ||||
-rw-r--r-- | customwebhookexample.py | 193 |
3 files changed, 236 insertions, 13 deletions
@@ -1,12 +1,40 @@ | |||
1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
2 | 2 | ||
3 | from telegram.ext import Application, CallbackQueryHandler, CommandHandler, MessageHandler, filters, ConversationHandler | ||
4 | import logging | 3 | import logging |
5 | from callback import callback_skip_media, callback_location_sharing, callback_manual_location, \ | 4 | |
6 | callback_location_confirmation, callback_location_keyword_search, callback_skip_location_keyword, \ | 5 | from telegram.ext import ( |
7 | callback_add_comment, callback_skip_comment, callback_add_media | 6 | Application, |
8 | from config import WAIT_LOCATION, LOCATION_SEARCH_KEYWORD, LOCATION_CONFIRMATION, ADD_MEDIA, ADD_COMMENT, BOT_TOKEN | 7 | CallbackQueryHandler, |
9 | from command import start_command, cancel_command, help_command | 8 | CommandHandler, |
9 | MessageHandler, | ||
10 | filters, | ||
11 | ConversationHandler | ||
12 | ) | ||
13 | |||
14 | from callback import ( | ||
15 | callback_skip_media, | ||
16 | callback_location_sharing, | ||
17 | callback_manual_location, | ||
18 | callback_location_confirmation, | ||
19 | callback_location_keyword_search, | ||
20 | callback_skip_location_keyword, | ||
21 | callback_add_comment, | ||
22 | callback_skip_comment, | ||
23 | callback_add_media | ||
24 | ) | ||
25 | from command import ( | ||
26 | start_command, | ||
27 | cancel_command, | ||
28 | help_command | ||
29 | ) | ||
30 | from config import ( | ||
31 | WAIT_LOCATION, | ||
32 | LOCATION_SEARCH_KEYWORD, | ||
33 | LOCATION_CONFIRMATION, | ||
34 | ADD_MEDIA, | ||
35 | ADD_COMMENT, | ||
36 | BOT_TOKEN | ||
37 | ) | ||
10 | 38 | ||
11 | logging.basicConfig( | 39 | logging.basicConfig( |
12 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO | 40 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO |
diff --git a/callback.py b/callback.py index 687508a..a7f7181 100644 --- a/callback.py +++ b/callback.py | |||
@@ -1,14 +1,16 @@ | |||
1 | import io | 1 | import io |
2 | from typing import cast, List | ||
3 | |||
4 | from telegram import ReplyKeyboardRemove | ||
5 | from telegram.constants import ChatAction | ||
6 | from telegram.error import BadRequest | ||
7 | from telegram.ext import CallbackContext | ||
8 | |||
9 | from command import * | ||
10 | from dbstore.dbm_store import get_loc | ||
2 | from foursquare.poi import OSM_ENDPOINT | 11 | from foursquare.poi import OSM_ENDPOINT |
3 | from foursquare.poi import query_poi | 12 | from foursquare.poi import query_poi |
4 | from dbstore.dbm_store import get_loc | ||
5 | from toot import mastodon_client | 13 | from toot import mastodon_client |
6 | from command import * | ||
7 | from typing import cast, List | ||
8 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardRemove | ||
9 | from telegram.ext import ContextTypes, ConversationHandler, CallbackContext | ||
10 | from telegram.constants import ParseMode, ChatAction | ||
11 | from telegram.error import BadRequest | ||
12 | 14 | ||
13 | 15 | ||
14 | async def callback_skip_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | 16 | async def callback_skip_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
diff --git a/customwebhookexample.py b/customwebhookexample.py new file mode 100644 index 0000000..328a327 --- /dev/null +++ b/customwebhookexample.py | |||
@@ -0,0 +1,193 @@ | |||
1 | #!/usr/bin/env python | ||
2 | # This program is dedicated to the public domain under the CC0 license. | ||
3 | # pylint: disable=import-error,wrong-import-position | ||
4 | """ | ||
5 | Simple example of a bot that uses a custom webhook setup and handles custom updates. | ||
6 | For the custom webhook setup, the libraries `starlette` and `uvicorn` are used. Please install | ||
7 | them as `pip install starlette~=0.20.0 uvicorn~=0.17.0`. | ||
8 | Note that any other `asyncio` based web server framework can be used for a custom webhook setup | ||
9 | just as well. | ||
10 | |||
11 | Usage: | ||
12 | Set bot token, url, admin chat_id and port at the start of the `main` function. | ||
13 | You may also need to change the `listen` value in the uvicorn configuration to match your setup. | ||
14 | Press Ctrl-C on the command line or send a signal to the process to stop the bot. | ||
15 | """ | ||
16 | import asyncio | ||
17 | import html | ||
18 | import logging | ||
19 | from dataclasses import dataclass | ||
20 | from http import HTTPStatus | ||
21 | from config import BOT_TOKEN | ||
22 | |||
23 | import uvicorn | ||
24 | from starlette.applications import Starlette | ||
25 | from starlette.requests import Request | ||
26 | from starlette.responses import PlainTextResponse, Response | ||
27 | from starlette.routing import Route | ||
28 | |||
29 | from telegram import __version__ as TG_VER | ||
30 | |||
31 | try: | ||
32 | from telegram import __version_info__ | ||
33 | except ImportError: | ||
34 | __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment] | ||
35 | |||
36 | if __version_info__ < (20, 0, 0, "alpha", 1): | ||
37 | raise RuntimeError( | ||
38 | f"This example is not compatible with your current PTB version {TG_VER}. To view the " | ||
39 | f"{TG_VER} version of this example, " | ||
40 | f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html" | ||
41 | ) | ||
42 | |||
43 | from telegram import Update | ||
44 | from telegram.constants import ParseMode | ||
45 | from telegram.ext import ( | ||
46 | Application, | ||
47 | CallbackContext, | ||
48 | CommandHandler, | ||
49 | ContextTypes, | ||
50 | ExtBot, | ||
51 | TypeHandler, | ||
52 | ) | ||
53 | |||
54 | # Enable logging | ||
55 | logging.basicConfig( | ||
56 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO | ||
57 | ) | ||
58 | logger = logging.getLogger(__name__) | ||
59 | |||
60 | |||
61 | @dataclass | ||
62 | class WebhookUpdate: | ||
63 | """Simple dataclass to wrap a custom update type""" | ||
64 | |||
65 | user_id: int | ||
66 | payload: str | ||
67 | |||
68 | |||
69 | class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): | ||
70 | """ | ||
71 | Custom CallbackContext class that makes `user_data` available for updates of type | ||
72 | `WebhookUpdate`. | ||
73 | """ | ||
74 | |||
75 | @classmethod | ||
76 | def from_update( | ||
77 | cls, | ||
78 | update: object, | ||
79 | application: "Application", | ||
80 | ) -> "CustomContext": | ||
81 | if isinstance(update, WebhookUpdate): | ||
82 | return cls(application=application, user_id=update.user_id) | ||
83 | return super().from_update(update, application) | ||
84 | |||
85 | |||
86 | async def start(update: Update, context: CustomContext) -> None: | ||
87 | """Display a message with instructions on how to use this bot.""" | ||
88 | url = context.bot_data["url"] | ||
89 | payload_url = html.escape(f"{url}/submitpayload?user_id=<your user id>&payload=<payload>") | ||
90 | text = ( | ||
91 | f"To check if the bot is still running, call <code>{url}/healthcheck</code>.\n\n" | ||
92 | f"To post a custom update, call <code>{payload_url}</code>." | ||
93 | ) | ||
94 | await update.message.reply_html(text=text) | ||
95 | |||
96 | |||
97 | async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: | ||
98 | """Callback that handles the custom updates.""" | ||
99 | chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) | ||
100 | payloads = context.user_data.setdefault("payloads", []) | ||
101 | payloads.append(update.payload) | ||
102 | combined_payloads = "</code>\n• <code>".join(payloads) | ||
103 | text = ( | ||
104 | f"The user {chat_member.user.mention_html()} has sent a new payload. " | ||
105 | f"So far they have sent the following payloads: \n\n• <code>{combined_payloads}</code>" | ||
106 | ) | ||
107 | await context.bot.send_message( | ||
108 | chat_id=context.bot_data["admin_chat_id"], text=text, parse_mode=ParseMode.HTML | ||
109 | ) | ||
110 | |||
111 | |||
112 | async def main() -> None: | ||
113 | """Set up the application and a custom webserver.""" | ||
114 | url = "https://jinwei.me" | ||
115 | admin_chat_id = 123456 | ||
116 | port = 8000 | ||
117 | |||
118 | context_types = ContextTypes(context=CustomContext) | ||
119 | # Here we set updater to None because we want our custom webhook server to handle the updates | ||
120 | # and hence we don't need an Updater instance | ||
121 | application = ( | ||
122 | Application.builder().token(BOT_TOKEN).updater(None).context_types(context_types).build() | ||
123 | ) | ||
124 | # save the values in `bot_data` such that we may easily access them in the callbacks | ||
125 | application.bot_data["url"] = url | ||
126 | application.bot_data["admin_chat_id"] = admin_chat_id | ||
127 | |||
128 | # register handlers | ||
129 | application.add_handler(CommandHandler("start", start)) | ||
130 | application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) | ||
131 | |||
132 | # Pass webhook settings to telegram | ||
133 | await application.bot.set_webhook(url=f"{url}/telegram") | ||
134 | |||
135 | # Set up webserver | ||
136 | async def telegram(request: Request) -> Response: | ||
137 | """Handle incoming Telegram updates by putting them into the `update_queue`""" | ||
138 | await application.update_queue.put( | ||
139 | Update.de_json(data=await request.json(), bot=application.bot) | ||
140 | ) | ||
141 | return Response() | ||
142 | |||
143 | async def custom_updates(request: Request) -> PlainTextResponse: | ||
144 | """ | ||
145 | Handle incoming webhook updates by also putting them into the `update_queue` if | ||
146 | the required parameters were passed correctly. | ||
147 | """ | ||
148 | try: | ||
149 | user_id = int(request.query_params["user_id"]) | ||
150 | payload = request.query_params["payload"] | ||
151 | except KeyError: | ||
152 | return PlainTextResponse( | ||
153 | status_code=HTTPStatus.BAD_REQUEST, | ||
154 | content="Please pass both `user_id` and `payload` as query parameters.", | ||
155 | ) | ||
156 | except ValueError: | ||
157 | return PlainTextResponse( | ||
158 | status_code=HTTPStatus.BAD_REQUEST, | ||
159 | content="The `user_id` must be a string!", | ||
160 | ) | ||
161 | |||
162 | await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) | ||
163 | return PlainTextResponse("Thank you for the submission! It's being forwarded.") | ||
164 | |||
165 | async def health(_: Request) -> PlainTextResponse: | ||
166 | """For the health endpoint, reply with a simple plain text message.""" | ||
167 | return PlainTextResponse(content="The bot is still running fine :)") | ||
168 | |||
169 | starlette_app = Starlette( | ||
170 | routes=[ | ||
171 | Route("/telegram", telegram, methods=["POST"]), | ||
172 | Route("/healthcheck", health, methods=["GET"]), | ||
173 | Route("/submitpayload", custom_updates, methods=["POST", "GET"]), | ||
174 | ] | ||
175 | ) | ||
176 | webserver = uvicorn.Server( | ||
177 | config=uvicorn.Config( | ||
178 | app=starlette_app, | ||
179 | port=port, | ||
180 | use_colors=False, | ||
181 | host="127.0.0.1", | ||
182 | ) | ||
183 | ) | ||
184 | |||
185 | # Run application and webserver together | ||
186 | async with application: | ||
187 | await application.start() | ||
188 | await webserver.serve() | ||
189 | await application.stop() | ||
190 | |||
191 | |||
192 | if __name__ == "__main__": | ||
193 | asyncio.run(main()) | ||