diff --git a/telebot/__init__.py b/telebot/__init__.py index 9e6e124..6c793af 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -18,6 +18,13 @@ import telebot.types from telebot.storage import StatePickleStorage, StateMemoryStorage +# random module to generate random string +import random +import string + + +# webhooks module +from telebot.extensions.webhooks import SyncWebhookListener logger = logging.getLogger('TeleBot') @@ -293,6 +300,74 @@ class TeleBot: return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates, ip_address, drop_pending_updates, timeout, secret_token) + + def run_webhooks(self, + listen: Optional[str]="127.0.0.1", + port: Optional[int]=443, + url_path: Optional[str]=None, + certificate: Optional[str]=None, + certificate_key: Optional[str]=None, + webhook_url: Optional[str]=None, + max_connections: Optional[int]=None, + allowed_updates: Optional[List]=None, + ip_address: Optional[str]=None, + drop_pending_updates: Optional[bool] = None, + timeout: Optional[int]=None, + secret_token: Optional[str]=None, + secret_token_length: Optional[int]=20, + debug: Optional[bool]=False): + """ + This class sets webhooks and listens to a given url and port. + + :param listen: IP address to listen to. Defaults to + 0.0.0.0 + :param port: A port which will be used to listen to webhooks. + :param url_path: Path to the webhook. Defaults to /token + :param certificate: Path to the certificate file. + :param certificate_key: Path to the certificate key file. + :param webhook_url: Webhook URL. + :param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput. + :param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used. + :param ip_address: The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS + :param drop_pending_updates: Pass True to drop all pending updates + :param timeout: Integer. Request connection timeout + :param secret_token: Secret token to be used to verify the webhook request. + :return: + """ + + # generate secret token if not set + if not secret_token: + secret_token = ''.join(random.choices(string.ascii_uppercase + string.digits, k=secret_token_length)) + + + if not url_path: + url_path = self.token + '/' + if url_path[-1] != '/': url_path += '/' + + + + protocol = "https" if certificate else "http" + if not webhook_url: + webhook_url = "{}://{}:{}/{}".format(protocol, listen, port, url_path) + + + self.set_webhook( + url=webhook_url, + certificate=certificate, + max_connections=max_connections, + allowed_updates=allowed_updates, + ip_address=ip_address, + drop_pending_updates=drop_pending_updates, + timeout=timeout, + secret_token=secret_token + ) + + ssl_context = (certificate, certificate_key) if certificate else None + self.webhook_listener = SyncWebhookListener(self, secret_token, listen, port, ssl_context, '/'+url_path, debug) + self.webhook_listener.run_app() + return self.webhook_listener + + def delete_webhook(self, drop_pending_updates=None, timeout=None): """ Use this method to remove webhook integration if you decide to switch back to getUpdates. diff --git a/telebot/extensions/webhooks.py b/telebot/extensions/webhooks.py new file mode 100644 index 0000000..e1c8e32 --- /dev/null +++ b/telebot/extensions/webhooks.py @@ -0,0 +1,87 @@ +""" +This file is used by TeleBot.run_webhooks() & +AsyncTeleBot.run_webhooks() functions. + +Flask/Aiohttp is required to run this script. +""" + + +flask_installed = True +try: + import flask + from werkzeug.serving import _TSSLContextArg +except ImportError: + flask_installed = False + + +from telebot.types import Update + + +from typing import Optional + + + + + +class SyncWebhookListener: + def __init__(self, bot, + secret_token: str, host: Optional[str]="127.0.0.1", + port: Optional[int]=8000, + ssl_context: Optional[_TSSLContextArg]=None, + url_path: Optional[str]=None, + debug: Optional[bool]=False + ) -> None: + """ + Synchronous implementation of webhook listener + for synchronous version of telebot. + + :param bot: TeleBot instance + :param secret_token: Telegram secret token + :param host: Webhook host + :param port: Webhook port + :param ssl_context: SSL context + """ + if not flask_installed: + raise ImportError('Flask is not installed. Please install it via pip.') + self.app = flask.Flask(__name__) + self._secret_token = secret_token + self._bot = bot + self._port = port + self._host = host + self._ssl_context = ssl_context + self._url_path = url_path + self._debug = debug + self._prepare_endpoint_urls() + + + def _prepare_endpoint_urls(self): + self.app.add_url_rule(self._url_path, 'index', self.process_update, methods=['POST']) + + + def process_update(self): + """ + Processes updates. + """ + # header containsX-Telegram-Bot-Api-Secret-Token + if flask.request.headers.get('X-Telegram-Bot-Api-Secret-Token') != self._secret_token: + # secret token didn't match + flask.abort(403) + if flask.request.headers.get('content-type') == 'application/json': + json_string = flask.request.get_data().decode('utf-8') + self._bot.process_new_updates([Update.de_json(json_string)]) + return '' + + flask.abort(403) + + + def run_app(self): + """ + Run app with the given parameters. + """ + self.app.run( + host=self._host, + port=self._port, + ssl_context=self._ssl_context, + debug=self._debug + ) + return self \ No newline at end of file