From 0e76b6c9f3356aee5cc7a923eb9050db6214ecaf Mon Sep 17 00:00:00 2001 From: "M. Hosseyn Najafi" Date: Mon, 20 Jul 2020 13:38:11 +0430 Subject: [PATCH] Organize code and add new DISABLE_SETUP configuration option (#9) * Add DISABLE_SETUP option to configuration * Change structure of storing bots internally * Use BotData class instead of dict for storing data --- README.rst | 11 ++- django_telegrambot/apps.py | 184 +++++++++++++++++++----------------- django_telegrambot/bot.py | 25 +++++ django_telegrambot/views.py | 2 +- sampleproject/bot/views.py | 2 +- 5 files changed, 134 insertions(+), 90 deletions(-) create mode 100644 django_telegrambot/bot.py diff --git a/README.rst b/README.rst index e9a093c..425072d 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ django-telegrambot .. image:: https://img.shields.io/badge/Donate-PayPal-green.svg :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LMXQVQ3YA2JJQ - + .. image:: http://pepy.tech/badge/django-telegrambot :target: http://pepy.tech/count/django-telegrambot @@ -77,10 +77,16 @@ And set your bots:: # apps contain telegrambot.py files that cannot be successfully # imported. + 'DISABLE_SETUP': False, # If set to True, there will be no tries to set webhook or read + # updates from the telegram server on app's start + # (useful when developing on local machine; makes django's startup faster) + 'BOT_MODULE_NAME': 'telegrambot_handlers', #(Optional [str]) # The default name for file name containing telegram handlers which has to be placed inside your local app(s). Default is 'telegrambot'. Example is to put "telegrambot_handlers.py" file to local app's folder. 'BOTS' : [ { + 'ID': 'MainBot', #Unique identifier for your bot (used in your code only) + 'TOKEN': '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', #Your bot token. #'CONTEXT': True, # Use context based handler functions @@ -104,7 +110,7 @@ And set your bots:: #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. - + # 'MESSAGEQUEUE_ENABLED':(Optinal[bool]), # Make this True if you want to use messagequeue # 'MESSAGEQUEUE_ALL_BURST_LIMIT':(Optional[int]), # If not provided 29 is the default value @@ -180,6 +186,7 @@ Then use it in a project creating a module ``telegrambot.py`` in your app :: # Default dispatcher (this is related to the first bot in settings.DJANGO_TELEGRAMBOT['BOTS']) dp = DjangoTelegramBot.dispatcher # To get Dispatcher related to a specific bot + # dp = DjangoTelegramBot.getDispatcher('BOT_n_id') #get by bot identifier # dp = DjangoTelegramBot.getDispatcher('BOT_n_token') #get by bot token # dp = DjangoTelegramBot.getDispatcher('BOT_n_username') #get by bot username diff --git a/django_telegrambot/apps.py b/django_telegrambot/apps.py index 5b40d50..3b6843f 100644 --- a/django_telegrambot/apps.py +++ b/django_telegrambot/apps.py @@ -2,6 +2,9 @@ # django_telegram_bot/apps.py import os.path import importlib +from collections import OrderedDict +from typing import List + import telegram import logging from time import sleep @@ -19,6 +22,7 @@ from telegram.utils.request import Request from telegram.ext import messagequeue as mq +from .bot import BotData from .mqbot import MQBot @@ -43,42 +47,61 @@ class DjangoTelegramBot(AppConfig): name = 'django_telegrambot' verbose_name = 'Django TelegramBot' ready_run = False - bot_tokens = [] - bot_usernames = [] - dispatchers = [] - bots = [] - updaters = [] + bots_data: List[BotData] = list() __used_tokens = set() @classproperty def dispatcher(cls): - #print("Getting value default dispatcher") - cls.__used_tokens.add(cls.bot_tokens[0]) - return cls.dispatchers[0] + try: + #print("Getting value default dispatcher") + bot_data = cls.bots_data[0] + cls.__used_tokens.add(bot_data.token) + return bot_data.dispatcher + except (StopIteration, IndexError): + raise ReferenceError("No bots are defined") @classproperty def updater(cls): - #print("Getting value default updater") - cls.__used_tokens.add(cls.bot_tokens[0]) - return cls.updaters[0] + try: + #print("Getting value default dispatcher") + bot_data = cls.bots_data[0] + cls.__used_tokens.add(bot_data.token) + return bot_data.updater + except (StopIteration, IndexError): + raise ReferenceError("No bots are defined") + @classmethod - def get_dispatcher(cls, bot_id=None, safe=True): + def _get_bot_by_id(cls, bot_id=None, safe=True): if bot_id is None: - cls.__used_tokens.add(cls.bot_tokens[0]) - return cls.dispatchers[0] + try: + return cls.bots_data[0] + except IndexError: + return None else: try: - index = cls.bot_tokens.index(bot_id) - except ValueError: + bot = next(filter(lambda bot_data: bot_data.token == bot_id, cls.bots_data)) + except StopIteration: if not safe: return None try: - index = cls.bot_usernames.index(bot_id) - except ValueError: - return None - cls.__used_tokens.add(cls.bot_tokens[index]) - return cls.dispatchers[index] + bot = next(filter(lambda bot_data: bot_data.unique_id == bot_id, cls.bots_data)) + except StopIteration: + try: + bot = next(filter(lambda bot_data: bot_data.instance.username == bot_id, cls.bots_data)) + except StopIteration: + return None + cls.__used_tokens.add(bot.token) + return bot + + + @classmethod + def get_dispatcher(cls, bot_id=None, safe=True): + bot = cls._get_bot_by_id(bot_id, safe) + if bot: + return bot.dispatcher + else: + return None @classmethod @@ -88,22 +111,11 @@ def getDispatcher(cls, bot_id=None, safe=True): @classmethod def get_bot(cls, bot_id=None, safe=True): - if bot_id is None: - if safe: - return cls.bots[0] - else: - return None + bot = cls._get_bot_by_id(bot_id, safe) + if bot: + return bot.instance else: - try: - index = cls.bot_tokens.index(bot_id) - except ValueError: - if not safe: - return None - try: - index = cls.bot_usernames.index(bot_id) - except ValueError: - return None - return cls.bots[index] + return None @classmethod @@ -113,19 +125,11 @@ def getBot(cls, bot_id=None, safe=True): @classmethod def get_updater(cls, bot_id=None, safe=True): - if bot_id is None: - return cls.updaters[0] + bot = cls._get_bot_by_id(bot_id, safe) + if bot: + return bot.updater else: - try: - index = cls.bot_tokens.index(bot_id) - except ValueError: - if not safe: - return None - try: - index = cls.bot_usernames.index(bot_id) - except ValueError: - return None - return cls.updaters[index] + return None @classmethod @@ -170,43 +174,44 @@ def ready(self): logger.error('WEBHOOK_CERTIFICATE not found in {} '.format(cert)) for b in bots_list: - token = b.get('TOKEN', None) - context = b.get('CONTEXT', False) - if not token: - break - - allowed_updates = b.get('ALLOWED_UPDATES', None) - timeout = b.get('TIMEOUT', None) - proxy = b.get('PROXY', None) + bot = BotData( + token=b['TOKEN'], + unique_id=b.get('ID', None), + use_context=b.get('CONTEXT', False), + allowed_updates=b.get('ALLOWED_UPDATES', None), + timeout=b.get('TIMEOUT', None), + proxy=b.get('PROXY', None), + ) if self.mode == WEBHOOK_MODE: try: if b.get('MESSAGEQUEUE_ENABLED',False): q = mq.MessageQueue(all_burst_limit=b.get('MESSAGEQUEUE_ALL_BURST_LIMIT',29), all_time_limit_ms=b.get('MESSAGEQUEUE_ALL_TIME_LIMIT_MS',1024)) - if proxy: - request = Request(proxy_url=proxy['proxy_url'], urllib3_proxy_kwargs=proxy['urllib3_proxy_kwargs'], con_pool_size=b.get('MESSAGEQUEUE_REQUEST_CON_POOL_SIZE',8)) + if bot.proxy: + request = Request(proxy_url=bot.proxy['proxy_url'], urllib3_proxy_kwargs=bot.proxy['urllib3_proxy_kwargs'], con_pool_size=b.get('MESSAGEQUEUE_REQUEST_CON_POOL_SIZE',8)) else: request = Request(con_pool_size=b.get('MESSAGEQUEUE_REQUEST_CON_POOL_SIZE',8)) - bot = MQBot(token, request=request, mqueue=q) + bot.instance = MQBot(bot.token, request=request, mqueue=q) else: request = None - if proxy: - request = Request(proxy_url=proxy['proxy_url'], urllib3_proxy_kwargs=proxy['urllib3_proxy_kwargs']) - bot = telegram.Bot(token=token, request=request) - - DjangoTelegramBot.dispatchers.append(Dispatcher(bot, None, workers=0, use_context=context)) - hookurl = '{}/{}/{}/'.format(webhook_site, webhook_base, token) - max_connections = b.get('WEBHOOK_MAX_CONNECTIONS', 40) - setted = bot.setWebhook(hookurl, certificate=certificate, timeout=timeout, max_connections=max_connections, allowed_updates=allowed_updates) - webhook_info = bot.getWebhookInfo() - real_allowed = webhook_info.allowed_updates if webhook_info.allowed_updates else ["ALL"] - - bot.more_info = webhook_info - logger.info('Telegram Bot <{}> setting webhook [ {} ] max connections:{} allowed updates:{} pending updates:{} : {}'.format(bot.username, webhook_info.url, webhook_info.max_connections, real_allowed, webhook_info.pending_update_count, setted)) - + if bot.proxy: + request = Request(proxy_url=bot.proxy['proxy_url'], urllib3_proxy_kwargs=bot.proxy['urllib3_proxy_kwargs']) + bot.instance = telegram.Bot(token=bot.token, request=request) + + bot.dispatcher = Dispatcher(bot.instance, None, workers=0, use_context=bot.use_context) + if not settings.DJANGO_TELEGRAMBOT.get('DISABLE_SETUP', False): + hookurl = '{}/{}/{}/'.format(webhook_site, webhook_base, bot.token) + max_connections = b.get('WEBHOOK_MAX_CONNECTIONS', 40) + setted = bot.instance.setWebhook(hookurl, certificate=certificate, timeout=bot.timeout, max_connections=max_connections, allowed_updates=bot.allowed_updates) + webhook_info = bot.instance.getWebhookInfo() + real_allowed = webhook_info.allowed_updates if webhook_info.allowed_updates else ["ALL"] + bot.more_info = webhook_info + logger.info('Telegram Bot <{}> setting webhook [ {} ] max connections:{} allowed updates:{} pending updates:{} : {}'.format(bot.instance.username, webhook_info.url, webhook_info.max_connections, real_allowed, webhook_info.pending_update_count, setted)) + else: + logger.info('Telegram Bot setting webhook without enabling receiving') except InvalidToken: - logger.error('Invalid Token : {}'.format(token)) + logger.error('Invalid Token : {}'.format(bot.token)) return except RetryAfter as er: logger.debug('Error: "{}". Will retry in {} seconds'.format( @@ -222,14 +227,20 @@ def ready(self): else: try: - updater = Updater(token=token, request_kwargs=proxy, use_context=context) - bot = updater.bot - bot.delete_webhook() - DjangoTelegramBot.updaters.append(updater) - DjangoTelegramBot.dispatchers.append(updater.dispatcher) - DjangoTelegramBot.__used_tokens.add(token) + if not settings.DJANGO_TELEGRAMBOT.get('DISABLE_SETUP', False): + bot.updater = Updater(token=bot.token, request_kwargs=bot.proxy, use_context=bot.use_context) + bot.instance = bot.updater.bot + bot.instance.delete_webhook() + bot.dispatcher = bot.updater.dispatcher + DjangoTelegramBot.__used_tokens.add(bot.token) + else: + request = None + if bot.proxy: + request = Request(proxy_url=bot.proxy['proxy_url'], urllib3_proxy_kwargs=bot.proxy['urllib3_proxy_kwargs']) + bot.instance = telegram.Bot(token=bot.token, request=request) + bot.dispatcher = Dispatcher(bot.instance, None, workers=0, use_context=bot.use_context) except InvalidToken: - logger.error('Invalid Token : {}'.format(token)) + logger.error('Invalid Token : {}'.format(bot.token)) return except RetryAfter as er: logger.debug('Error: "{}". Will retry in {} seconds'.format( @@ -243,12 +254,13 @@ def ready(self): logger.error('Error: "{}"'.format(er.message)) return - DjangoTelegramBot.bots.append(bot) - DjangoTelegramBot.bot_tokens.append(token) - DjangoTelegramBot.bot_usernames.append(bot.username) + DjangoTelegramBot.bots_data.append(bot) - - logger.debug('Telegram Bot <{}> set as default bot'.format(DjangoTelegramBot.bots[0].username)) + first_bot = DjangoTelegramBot.bots_data[0] + if not settings.DJANGO_TELEGRAMBOT.get('DISABLE_SETUP', False): + logger.debug('Telegram Bot <{}> set as default bot'.format(first_bot.instance.username)) + else: + logger.debug('Telegram Bot <{}> set as default bot'.format(first_bot.unique_id if first_bot.unique_id else first_bot.token)) def module_imported(module_name, method_name, execute): try: diff --git a/django_telegrambot/bot.py b/django_telegrambot/bot.py new file mode 100644 index 0000000..df6b4db --- /dev/null +++ b/django_telegrambot/bot.py @@ -0,0 +1,25 @@ +from telegram import Bot +from telegram.ext import Dispatcher, Updater + + +class BotData: + def __init__(self, + token, + *, + unique_id=None, + use_context=False, + allowed_updates=None, + timeout=None, + proxy=None, + instance: Bot = None, + dispatcher: Dispatcher = None, + updater: Updater = None): + self.token = token + self.unique_id = unique_id + self.use_context = use_context + self.allowed_updates = allowed_updates + self.timeout = timeout + self.proxy = proxy + self.instance = instance + self.dispatcher = dispatcher + self.updater = updater diff --git a/django_telegrambot/views.py b/django_telegrambot/views.py index 44328ae..c912913 100644 --- a/django_telegrambot/views.py +++ b/django_telegrambot/views.py @@ -19,7 +19,7 @@ @staff_member_required def home(request): - bot_list = DjangoTelegramBot.bots + bot_list = [bot.instance for bot in DjangoTelegramBot.bots_data] context = {'bot_list': bot_list, 'update_mode':settings.DJANGO_TELEGRAMBOT.get('MODE', 'WEBHOOK')} return render(request, 'django_telegrambot/index.html', context) diff --git a/sampleproject/bot/views.py b/sampleproject/bot/views.py index c01e3ae..123a418 100644 --- a/sampleproject/bot/views.py +++ b/sampleproject/bot/views.py @@ -4,6 +4,6 @@ # Create your views here. def index(request): - bot_list = DjangoTelegramBot.bots + bot_list = [bot.instance for bot in DjangoTelegramBot.bots_data] context = {'bot_list': bot_list, 'update_mode':settings.DJANGO_TELEGRAMBOT['MODE']} return render(request, 'bot/index.html', context)