diff --git a/davtelepot/__init__.py b/davtelepot/__init__.py index 3ee6fe4..8921f81 100644 --- a/davtelepot/__init__.py +++ b/davtelepot/__init__.py @@ -2,7 +2,7 @@ __author__ = "Davide Testa" __email__ = "davte@libero.it" __credits__ = "Marco Origlia" __license__ = "GNU General Public License v3.0" -__version__ = "1.2.5" +__version__ = "1.2.6" __maintainer__ = "Davide Testa" __contact__ = "t.me/davte" diff --git a/davtelepot/custombot.py b/davtelepot/custombot.py index 422b016..3e463b0 100644 --- a/davtelepot/custombot.py +++ b/davtelepot/custombot.py @@ -1,8 +1,14 @@ -"""This module conveniently subclasses third party telepot.aio.Bot, providing the following features. -- It prevents hitting Telegram flood limits by waiting between text and photo messages. -- It provides command, parser, button and other decorators to associate common Telegram actions with custom handlers. -- It supports multiple bots running in the same script and allows communications between them as well as complete independency from each other. -- Each bot is associated with a sqlite database (using dataset third party library). +"""Subclass of third party telepot.aio.Bot, providing the following features. + +- It prevents hitting Telegram flood limits by waiting + between text and photo messages. +- It provides command, parser, button and other decorators to associate + common Telegram actions with custom handlers. +- It supports multiple bots running in the same script + and allows communications between them + as well as complete independency from each other. +- Each bot is associated with a sqlite database + using dataset, a third party library. Please note that you need Python3.5+ to run async code Check requirements.txt for third party dependencies. @@ -17,46 +23,59 @@ import os # Third party modules import dataset -import telepot, telepot.aio +import telepot +import telepot.aio # Project modules from davteutil import Gettable, MyOD -from davteutil import escape_html_chars, get_cleaned_text, make_lines_of_buttons, markdown_check, remove_html_tags, sleep_until +from davteutil import (escape_html_chars, get_cleaned_text, + make_lines_of_buttons, markdown_check, + remove_html_tags, sleep_until) + def split_text_gracefully(text, limit, parse_mode): - """Split text if it hits telegram limits for text messages. + r"""Split text if it hits telegram limits for text messages. + Split at `\n` if possible. - Add a `[...]` at the end and beginning of split messages, with proper code markdown. + Add a `[...]` at the end and beginning of split messages, + with proper code markdown. """ text = text.split("\n")[::-1] result = [] - while len(text)>0: - temp=[] - while len(text)>0 and len("\n".join(temp + [text[-1]])) 0: + temp = [] + while len(text) > 0 and len("\n".join(temp + [text[-1]])) < limit: temp.append(text.pop()) if len(temp) == 0: temp.append(text[-1][:limit]) text[-1] = text[-1][limit:] result.append("\n".join(temp)) - if len(result)>1: - for i in range(1,len(result)): + if len(result) > 1: + for i in range(1, len(result)): result[i] = "{tag[0]}[...]{tag[1]}\n{text}".format( - tag=('`','`') if parse_mode=='Markdown' + tag=( + ('`', '`') if parse_mode == 'Markdown' else ('', '') if parse_mode.lower() == 'html' - else ('', ''), + else ('', '') + ), text=result[i] ) result[i-1] = "{text}\n{tag[0]}[...]{tag[1]}".format( - tag=('`','`') if parse_mode=='Markdown' + tag=( + ('`', '`') if parse_mode == 'Markdown' else ('', '') if parse_mode.lower() == 'html' - else ('', ''), + else ('', '') + ), text=result[i-1] ) return result + def make_inline_query_answer(answer): """Return an article-type answer to inline query. - Takes either a string or a dictionary and returns a list.""" + + Takes either a string or a dictionary and returns a list. + """ if type(answer) is str: answer = dict( type='article', @@ -71,25 +90,40 @@ def make_inline_query_answer(answer): answer = [answer] return answer + class Bot(telepot.aio.Bot, Gettable): """telepot.aio.Bot (async Telegram bot framework) convenient subclass. + === General functioning === - - While Bot.run() coroutine is executed, HTTP get requests are made to Telegram servers asking for new messages for each Bot instance. - - Each message causes the proper Bot instance method coroutine to be awaited, according to its flavour (see routing_table) - -- For example, chat messages cause `Bot().on_chat_message(message)` to be awaited. - - This even-processing coroutine ensures the proper handling function a future and returns. - -- That means that simpler tasks are completed before slower ones, since handling functions are not awaited but scheduled by `asyncio.ensure_future(handling_function(...))` - -- For example, chat text messages are handled by `handle_text_message`, which looks for the proper function to elaborate the request (in bot's commands and parsers) - - The handling function evaluates an answer, depending on the message content, and eventually provides a reply - -- For example, `handle_text_message` sends its answer via `send_message` - - All Bot.instances run simultaneously and faster requests are completed earlier. + - While Bot.run() coroutine is executed, HTTP get requests are made + to Telegram servers asking for new messages for each Bot instance. + - Each message causes the proper Bot instance method coroutine + to be awaited, according to its flavour (see routing_table) + -- For example, chat messages cause `Bot().on_chat_message(message)` + to be awaited. + - This even-processing coroutine ensures the proper handling function + a future and returns. + -- That means that simpler tasks are completed before slower ones, + since handling functions are not awaited but scheduled + by `asyncio.ensure_future(handling_function(...))` + -- For example, chat text messages are handled by + `handle_text_message`, which looks for the proper function + to elaborate the request (in bot's commands and parsers) + - The handling function evaluates an answer, depending on the message + content, and eventually provides a reply + -- For example, `handle_text_message` sends its + answer via `send_message` + - All Bot.instances run simultaneously and faster requests + are completed earlier. - All uncaught events are ignored. """ instances = {} stop = False - # Cooldown time between sent messages, to prevent hitting Telegram flood limits - # Current limits: 30 total messages sent per second, 1 message per second per chat, 20 messages per minute per group + # Cooldown time between sent messages, to prevent hitting + # Telegram flood limits + # Current limits: 30 total messages sent per second, + # 1 message per second per chat, 20 messages per minute per group COOLDOWN_TIME_ABSOLUTE = datetime.timedelta(seconds=1/30) COOLDOWN_TIME_PER_CHAT = datetime.timedelta(seconds=1) MAX_GROUP_MESSAGES_PER_MINUTE = 20 @@ -105,18 +139,20 @@ class Bot(telepot.aio.Bot, Gettable): id=0, title="I cannot answer this query, sorry", input_message_content=dict( - message_text="I'm sorry but I could not find an answer for your query." + message_text="I'm sorry " + "but I could not find an answer for your query." ) ) ] def __init__(self, token, db_name=None): + """Instantiate Bot instance, given a token and a db name.""" super().__init__(token) self.routing_table = { - 'chat' : self.on_chat_message, - 'inline_query' : self.on_inline_query, - 'chosen_inline_result' : self.on_chosen_inline_result, - 'callback_query' : self.on_callback_query + 'chat': self.on_chat_message, + 'inline_query': self.on_inline_query, + 'chosen_inline_result': self.on_chosen_inline_result, + 'callback_query': self.on_callback_query } self.chat_message_handlers = { 'text': self.handle_text_message, @@ -130,7 +166,11 @@ class Bot(telepot.aio.Bot, Gettable): ) self._unauthorized_message = None self.authorization_function = lambda update, authorization_level: True - self.get_chat_id = lambda update: update['message']['chat']['id'] if 'message' in update else update['chat']['id'] + self.get_chat_id = lambda update: ( + update['message']['chat']['id'] + if 'message' in update + else update['chat']['id'] + ) self.commands = dict() self.callback_handlers = dict() self.inline_query_handlers = MyOD() @@ -148,7 +188,10 @@ class Bot(telepot.aio.Bot, Gettable): self.to_be_obscured = [] self.to_be_destroyed = [] self.last_sending_time = dict( - absolute=datetime.datetime.now() - self.__class__.COOLDOWN_TIME_ABSOLUTE + absolute=( + datetime.datetime.now() + - self.__class__.COOLDOWN_TIME_ABSOLUTE + ) ) self._maintenance = False self._maintenance_message = None @@ -158,17 +201,18 @@ class Bot(telepot.aio.Bot, Gettable): @property def name(self): - """Bot name""" + """Bot name.""" return self.bot_name @property def path(self): - """custombot.py file path""" + """custombot.py file path.""" return self.__class__._path @property def db(self): - """Connection to bot's database + """Connect to bot's database. + It must be used inside a with statement: `with bot.db as db` """ if self.db_url: @@ -176,14 +220,15 @@ class Bot(telepot.aio.Bot, Gettable): @property def default_keyboard(self): - """Default keyboard which is sent when reply_markup is left blank and chat is private. + """Get the default keyboard. + + It is sent when reply_markup is left blank and chat is private. """ return self._default_keyboard @property def default_inline_query_answer(self): - """Answer to be returned if inline query returned None. - """ + """Answer to be returned if inline query returned None.""" if self._default_inline_query_answer: return self._default_inline_query_answer return self.__class__._default_inline_query_answer @@ -191,6 +236,7 @@ class Bot(telepot.aio.Bot, Gettable): @property def unauthorized_message(self): """Return this if user is unauthorized to make a request. + If instance message is not set, class message is returned. """ if self._unauthorized_message: @@ -200,6 +246,7 @@ class Bot(telepot.aio.Bot, Gettable): @property def unknown_command_message(self): """Message to be returned if user sends an unknown command in private chat. + If instance message is not set, class message is returned. """ if self._unknown_command_message: @@ -208,13 +255,17 @@ class Bot(telepot.aio.Bot, Gettable): @property def maintenance(self): - """True if bot is under maintenance, False otherwise. - While under maintenance, bot will reply with `self.maintenance_message` to any request, with few exceptions.""" + """Check whether bot is under maintenance. + + While under maintenance, bot will reply with + `self.maintenance_message` to any request, with few exceptions. + """ return self._maintenance @property def maintenance_message(self): """Message to be returned if bot is under maintenance. + If instance message is not set, class message is returned. """ if self._maintenance_message: @@ -226,120 +277,171 @@ class Bot(telepot.aio.Bot, Gettable): @classmethod def set_class_path(csl, path): """Set class path, where files will be looked for. - For example, if send_photo receives `photo='mypic.png'`, it will parse it as `'{path}/mypic.png'.format(path=self.path)` + + For example, if send_photo receives `photo='mypic.png'`, + it will parse it as `'{path}/mypic.png'.format(path=self.path)` """ csl._path = path @classmethod def set_class_unauthorized_message(csl, unauthorized_message): - """Set class unauthorized message, to be returned if user is unauthorized to make a request. + """Set class unauthorized message. + + It will be returned if user is unauthorized to make a request. """ csl._unauthorized_message = unauthorized_message @classmethod def set_class_unknown_command_message(cls, unknown_command_message): - """Set class unknown command message, to be returned if user sends an unknown command in private chat. + """Set class unknown command message. + + It will be returned if user sends an unknown command in private chat. """ cls._unknown_command_message = unknown_command_message @classmethod def set_class_maintenance_message(cls, maintenance_message): - """Set class maintenance message, to be returned if bot is under maintenance. + """Set class maintenance message. + + It will be returned if bot is under maintenance. """ cls._maintenance_message = maintenance_message @classmethod - def set_class_default_inline_query_answer(cls, default_inline_query_answer): - """Set class default inline query answer, to be returned if an inline query returned no answer. + def set_class_default_inline_query_answer(cls, + default_inline_query_answer): + """Set class default inline query answer. + + It will be returned if an inline query returned no answer. """ cls._default_inline_query_answer = default_inline_query_answer def set_unauthorized_message(self, unauthorized_message): - """Set instance unauthorized message + """Set instance unauthorized message. + If instance message is None, default class message is used. """ self._unauthorized_message = unauthorized_message def set_unknown_command_message(self, unknown_command_message): - """Set instance unknown command message, to be returned if user sends an unknown command in private chat. + """Set instance unknown command message. + + It will be returned if user sends an unknown command in private chat. If instance message is None, default class message is used. """ self._unknown_command_message = unknown_command_message def set_maintenance_message(self, maintenance_message): - """Set instance maintenance message, to be returned if bot is under maintenance. + """Set instance maintenance message. + + It will be returned if bot is under maintenance. If instance message is None, default class message is used. """ self._maintenance_message = maintenance_message def set_default_inline_query_answer(self, default_inline_query_answer): - """Set a custom default_inline_query_answer to be returned when no answer is found for an inline query. + """Set a custom default_inline_query_answer. + + It will be returned when no answer is found for an inline query. If instance answer is None, default class answer is used. """ if type(default_inline_query_answer) in (str, dict): - default_inline_query_answer = make_inline_query_answer(default_inline_query_answer) + default_inline_query_answer = make_inline_query_answer( + default_inline_query_answer + ) if type(default_inline_query_answer) is not list: return 1 self._default_inline_query_answer = default_inline_query_answer return 0 def set_maintenance(self, maintenance_message): - """Puts the bot under maintenance or ends it. + """Put the bot under maintenance or ends it. + While in maintenance, bot will reply to users with maintenance_message. Bot will accept /coma, /stop and /restart commands from admins. - s""" + """ self._maintenance = not self.maintenance if maintenance_message: self.set_maintenance_message(maintenance_message) if self.maintenance: - return "Bot has just been put under maintenance!\n\nUntil further notice, it will reply to users with the following message:\n\n{}".format( - self.maintenance_message - ) + return "Bot has just been put under maintenance!\n\n"\ + "Until further notice, it will reply to users "\ + "with the following message:\n\n{}".format( + self.maintenance_message + ) return "Maintenance ended!" def set_authorization_function(self, authorization_function): - """Set a custom authorization_function, which evaluates True if user is authorized to perform a specific action and False otherwise. + """Set a custom authorization_function. + + It should evaluate True if user is authorized to perform + a specific action and False otherwise. It should take update and role and return a Boolean. Default authorization_function always evaluates True. """ self.authorization_function = authorization_function def set_get_chat_id_function(self, get_chat_id_function): - """Set a custom get_chat_id function which takes and update and returns the chat in which a reply should be sent. - For instance, bots could reply in private to group messages as a default behaviour. + """Set a custom get_chat_id function. + + It should take and update and return the chat in which + a reply should be sent. + For instance, a bot could reply in private to group messages + as a default behaviour. Default chat_id returned is current chat id. """ self.get_chat_id = get_chat_id_function async def avoid_flooding(self, chat_id): """asyncio-sleep until COOLDOWN_TIME (per_chat and absolute) has passed. - To prevent hitting Telegram flood limits, send_message and send_photo await this function. + + To prevent hitting Telegram flood limits, send_message and + send_photo await this function. """ if type(chat_id) is int and chat_id > 0: while ( - datetime.datetime.now() < self.last_sending_time['absolute'] + self.__class__.COOLDOWN_TIME_ABSOLUTE + datetime.datetime.now() < ( + self.last_sending_time['absolute'] + + self.__class__.COOLDOWN_TIME_ABSOLUTE + ) ) or ( chat_id in self.last_sending_time - and datetime.datetime.now() < self.last_sending_time[chat_id] + self.__class__.COOLDOWN_TIME_PER_CHAT - ): - await asyncio.sleep(self.__class__.COOLDOWN_TIME_ABSOLUTE.seconds) + and ( + datetime.datetime.now() < ( + self.last_sending_time[chat_id] + + self.__class__.COOLDOWN_TIME_PER_CHAT + ) + ) + ): + await asyncio.sleep( + self.__class__.COOLDOWN_TIME_ABSOLUTE.seconds + ) self.last_sending_time[chat_id] = datetime.datetime.now() else: while ( - datetime.datetime.now() < self.last_sending_time['absolute'] + self.__class__.COOLDOWN_TIME_ABSOLUTE + datetime.datetime.now() < ( + self.last_sending_time['absolute'] + + self.__class__.COOLDOWN_TIME_ABSOLUTE + ) ) or ( chat_id in self.last_sending_time and len( [ sending_datetime for sending_datetime in self.last_sending_time[chat_id] - if sending_datetime >= datetime.datetime.now() - datetime.timedelta(minutes=1) + if sending_datetime >= ( + datetime.datetime.now() + - datetime.timedelta(minutes=1) + ) ] ) >= self.__class__.MAX_GROUP_MESSAGES_PER_MINUTE ) or ( chat_id in self.last_sending_time and len(self.last_sending_time[chat_id]) > 0 - and datetime.datetime.now() < self.last_sending_time[chat_id][-1] + self.__class__.COOLDOWN_TIME_PER_CHAT + and datetime.datetime.now() < ( + self.last_sending_time[chat_id][-1] + + self.__class__.COOLDOWN_TIME_PER_CHAT + ) ): await asyncio.sleep(0.5) if chat_id not in self.last_sending_time: @@ -348,33 +450,43 @@ class Bot(telepot.aio.Bot, Gettable): self.last_sending_time[chat_id] = [ sending_datetime for sending_datetime in self.last_sending_time[chat_id] - if sending_datetime >= datetime.datetime.now() - datetime.timedelta(minutes=1) + if sending_datetime >= ( + datetime.datetime.now() + - datetime.timedelta(minutes=1) + ) ] self.last_sending_time['absolute'] = datetime.datetime.now() return async def on_inline_query(self, update): """Schedule handling of received inline queries. + Notice that handling is only scheduled, not awaited. - This means that all Bot instances may now handle other requests before this one is completed. + This means that all Bot instances may now handle other requests + before this one is completed. """ asyncio.ensure_future(self.handle_inline_query(update)) return async def on_chosen_inline_result(self, update): """Schedule handling of received chosen inline result events. + Notice that handling is only scheduled, not awaited. - This means that all Bot instances may now handle other requests before this one is completed. + This means that all Bot instances may now handle other requests + before this one is completed. """ asyncio.ensure_future(self.handle_chosen_inline_result(update)) return async def on_callback_query(self, update): """Schedule handling of received callback queries. + A callback query is sent when users press inline keyboard buttons. - Bad clients may send malformed or deceiving callback queries: never use secret keys in buttons and always check request validity! + Bad clients may send malformed or deceiving callback queries: + never use secret keys in buttons and always check request validity! Notice that handling is only scheduled, not awaited. - This means that all Bot instances may now handle other requests before this one is completed. + This means that all Bot instances may now handle other requests + before this one is completed. """ # Reject malformed updates lacking of data field if 'data' not in update: @@ -384,9 +496,12 @@ class Bot(telepot.aio.Bot, Gettable): async def on_chat_message(self, update): """Schedule handling of received chat message. + Notice that handling is only scheduled, not awaited. - According to update type, the corresponding handler is scheduled (see self.chat_message_handlers). - This means that all Bot instances may now handle other requests before this one is completed. + According to update type, the corresponding handler is + scheduled (see self.chat_message_handlers). + This means that all Bot instances may now handle other + requests before this one is completed. """ answer = None content_type, chat_type, chat_id = telepot.glance( @@ -404,8 +519,7 @@ class Bot(telepot.aio.Bot, Gettable): return answer async def handle_inline_query(self, update): - """Handle inline query and answer it with results, or log errors. - """ + """Handle inline query and answer it with results, or log errors.""" query = update['query'] answer = None switch_pm_text, switch_pm_parameter = None, None @@ -444,7 +558,11 @@ class Bot(telepot.aio.Bot, Gettable): return async def handle_chosen_inline_result(self, update): - """If chosen inline result id is in self.chosen_inline_result_handlers, call the related function passing the update as argument.""" + """When an inline query result is chosen, perform an action. + + If chosen inline result id is in self.chosen_inline_result_handlers, + call the related function passing the update as argument. + """ user_id = update['from']['id'] if 'from' in update else None if self.maintenance: return @@ -460,11 +578,16 @@ class Bot(telepot.aio.Bot, Gettable): return def set_inline_result_handler(self, user_id, result_id, func): - """Associate a func to a result_id: when an inline result is chosen having that id, function will be passed the update as argument.""" + """Associate a func to a result_id. + + When an inline result is chosen having that id, function will + be passed the update as argument. + """ if type(user_id) is dict: user_id = user_id['from']['id'] assert type(user_id) is int, "user_id must be int!" - result_id = str(result_id) # Query result ids are parsed as str by telegram + # Query result ids are parsed as str by telegram + result_id = str(result_id) assert callable(func), "func must be a callable" if user_id not in self.chosen_inline_result_handlers: self.chosen_inline_result_handlers[user_id] = {} @@ -472,9 +595,13 @@ class Bot(telepot.aio.Bot, Gettable): return async def handle_callback_query(self, update): - """Get an answer from the callback handler associated to the query prefix. - The answer is used to edit the source message or send new ones if text is longer than single message limit. - Anyway, the query is answered, otherwise the client would hang and the bot would look like idle. + """Answer callback queries. + + Call the callback handler associated to the query prefix. + The answer is used to edit the source message or send new ones + if text is longer than single message limit. + Anyway, the query is answered, otherwise the client would hang and + the bot would look like idle. """ answer = None if self.maintenance: @@ -496,32 +623,61 @@ class Bot(telepot.aio.Bot, Gettable): return if 'edit' in answer: if 'message' in update: - message_identifier = telepot.message_identifier(update['message']) + message_identifier = telepot.message_identifier( + update['message'] + ) else: message_identifier = telepot.message_identifier(update) edit = answer['edit'] - reply_markup = edit['reply_markup'] if 'reply_markup' in edit else None - text = edit['text'] if 'text' in edit else None - caption = edit['caption'] if 'caption' in edit else None - parse_mode = edit['parse_mode'] if 'parse_mode' in edit else None - disable_web_page_preview = edit['disable_web_page_preview'] if 'disable_web_page_preview' in edit else None + reply_markup = ( + edit['reply_markup'] + if 'reply_markup' in edit + else None + ) + text = ( + edit['text'] + if 'text' in edit + else None + ) + caption = ( + edit['caption'] + if 'caption' in edit + else None + ) + parse_mode = ( + edit['parse_mode'] + if 'parse_mode' in edit + else None + ) + disable_web_page_preview = ( + edit['disable_web_page_preview'] + if 'disable_web_page_preview' in edit + else None + ) try: if 'text' in edit: - if len(text) > self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200: + if ( + len(text) + > self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 200 + ): if 'from' in update: await self.send_message( chat_id=update['from']['id'], text=text, reply_markup=reply_markup, parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview + disable_web_page_preview=( + disable_web_page_preview + ) ) else: await self.editMessageText( msg_identifier=message_identifier, text=text, parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview, + disable_web_page_preview=( + disable_web_page_preview + ), reply_markup=reply_markup ) elif 'caption' in edit: @@ -538,8 +694,16 @@ class Bot(telepot.aio.Bot, Gettable): except Exception as e: logging.info("Message was not modified:\n{}".format(e)) text = answer['text'][:180] if 'text' in answer else None - show_alert = answer['show_alert'] if 'show_alert' in answer else None - cache_time = answer['cache_time'] if 'cache_time' in answer else None + show_alert = ( + answer['show_alert'] + if 'show_alert' in answer + else None + ) + cache_time = ( + answer['cache_time'] + if 'cache_time' in answer + else None + ) try: await self.answerCallbackQuery( callback_query_id=update['id'], @@ -558,19 +722,27 @@ class Bot(telepot.aio.Bot, Gettable): async def handle_text_message(self, update): """Answer to chat text messages. - 1) Ignore bot name (case-insensitive) and search bot custom parsers, commands, aliases and parsers for an answerer. + + 1) Ignore bot name (case-insensitive) and search bot custom parsers, + commands, aliases and parsers for an answerer. 2) Get an answer from answerer(update). 3) Send it to the user. """ answerer, answer = None, None - # Lower text and replace only bot's tag, meaning that `/command@OtherBot` will be ignored. - text = update['text'].lower().replace('@{}'.format(self.name.lower()),'') + # Lower text and replace only bot's tag, + # meaning that `/command@OtherBot` will be ignored. + text = update['text'].lower().replace( + '@{}'.format( + self.name.lower() + ), + '' + ) user_id = update['from']['id'] if 'from' in update else None if self.maintenance and not any( text.startswith(x) for x in ('/coma', '/restart') ): - if update['chat']['id']>0: + if update['chat']['id'] > 0: answer = self.maintenance_message elif user_id in self.custom_parsers: answerer = self.custom_parsers[user_id] @@ -579,10 +751,11 @@ class Bot(telepot.aio.Bot, Gettable): command = text.split()[0].strip(' /@') if command in self.commands: answerer = self.commands[command]['function'] - elif update['chat']['id']>0: + elif update['chat']['id'] > 0: answer = self.unknown_command_message else: - # If text starts with an alias (case insensitive: text and alias are both .lower()): + # If text starts with an alias + # Aliases are case insensitive: text and alias are both .lower() for alias, parser in self.aliases.items(): if text.startswith(alias.lower()): answerer = parser @@ -607,7 +780,10 @@ class Bot(telepot.aio.Bot, Gettable): try: return await self.send_message(answer=answer, chat_id=update) except Exception as e: - logging.error("Failed to process answer:\n{}".format(e), exc_info=True) + logging.error( + "Failed to process answer:\n{}".format(e), + exc_info=True + ) async def handle_pinned_message(self, update): """Handle pinned message chat action.""" @@ -626,17 +802,25 @@ class Bot(telepot.aio.Bot, Gettable): answer = answerer(update) if answer: try: - return await self.send_message(answer=answer, chat_id=update['chat']['id']) + return await self.send_message( + answer=answer, + chat_id=update['chat']['id'] + ) except Exception as e: - logging.error("Failed to process answer:\n{}".format(e), exc_info=True) + logging.error( + "Failed to process answer:\n{}".format( + e + ), + exc_info=True + ) return async def handle_photo_message(self, update): - """Handle photo chat message""" + """Handle photo chat message.""" user_id = update['from']['id'] if 'from' in update else None answerer, answer = None, None if self.maintenance: - if update['chat']['id']>0: + if update['chat']['id'] > 0: answer = self.maintenance_message elif user_id in self.custom_photo_parsers: answerer = self.custom_photo_parsers[user_id] @@ -650,64 +834,126 @@ class Bot(telepot.aio.Bot, Gettable): try: return await self.send_message(answer=answer, chat_id=update) except Exception as e: - logging.error("Failed to process answer:\n{}".format(e), exc_info=True) + logging.error( + "Failed to process answer:\n{}".format( + e + ), + exc_info=True + ) return def set_custom_parser(self, parser, update=None, user=None): """Set a custom parser for the user. - Any chat message update coming from the user will be handled by this custom parser instead of default parsers (commands, aliases and text parsers). - Custom parsers last one single use, but their handler can call this function to provide multiple tries. + + Any chat message update coming from the user will be handled by + this custom parser instead of default parsers (commands, aliases + and text parsers). + Custom parsers last one single use, but their handler can call this + function to provide multiple tries. """ if user and type(user) is int: pass elif type(update) is int: user = update elif type(user) is dict: - user = user['from']['id'] if 'from' in user and 'id' in user['from'] else None + user = ( + user['from']['id'] + if 'from' in user + and 'id' in user['from'] + else None + ) elif not user and type(update) is dict: - user = update['from']['id'] if 'from' in update and 'id' in update['from'] else None + user = ( + update['from']['id'] + if 'from' in update + and 'id' in update['from'] + else None + ) else: - raise TypeError('Invalid user.\nuser: {}\nupdate: {}'.format(user, update)) + raise TypeError( + 'Invalid user.\nuser: {}\nupdate: {}'.format( + user, + update + ) + ) if not type(user) is int: - raise TypeError('User {} is not an int id'.format(user)) + raise TypeError( + 'User {} is not an int id'.format( + user + ) + ) if not callable(parser): - raise TypeError('Parser {} is not a callable'.format(parser.__name__)) + raise TypeError( + 'Parser {} is not a callable'.format( + parser.__name__ + ) + ) self.custom_parsers[user] = parser return def set_custom_photo_parser(self, parser, update=None, user=None): """Set a custom photo parser for the user. - Any photo chat update coming from the user will be handled by this custom parser instead of default parsers. - Custom photo parsers last one single use, but their handler can call this function to provide multiple tries. + + Any photo chat update coming from the user will be handled by + this custom parser instead of default parsers. + Custom photo parsers last one single use, but their handler can + call this function to provide multiple tries. """ if user and type(user) is int: pass elif type(update) is int: user = update elif type(user) is dict: - user = user['from']['id'] if 'from' in user and 'id' in user['from'] else None + user = ( + user['from']['id'] + if 'from' in user + and 'id' in user['from'] + else None + ) elif not user and type(update) is dict: - user = update['from']['id'] if 'from' in update and 'id' in update['from'] else None + user = ( + update['from']['id'] + if 'from' in update + and 'id' in update['from'] + else None + ) else: - raise TypeError('Invalid user.\nuser: {}\nupdate: {}'.format(user, update)) + raise TypeError( + 'Invalid user.\nuser: {}\nupdate: {}'.format( + user, + update + ) + ) if not type(user) is int: - raise TypeError('User {} is not an int id'.format(user)) + raise TypeError( + 'User {} is not an int id'.format( + user + ) + ) if not callable(parser): - raise TypeError('Parser {} is not a callable'.format(parser.__name__)) + raise TypeError( + 'Parser {} is not a callable'.format( + parser.__name__ + ) + ) self.custom_photo_parsers[user] = parser return - def command(self, command, aliases=None, show_in_keyboard=False, descr="", auth='admin'): - """ + def command(self, command, aliases=None, show_in_keyboard=False, + descr="", auth='admin'): + """Define a bot command. + Decorator: `@bot.command(*args)` - When a message text starts with `/command[@bot_name]`, or with an alias, it gets passed to the decorated function. + When a message text starts with `/command[@bot_name]`, or with an + alias, it gets passed to the decorated function. `command` is the command name (with or without /) `aliases` is a list of aliases - `show_in_keyboard`, if True, makes first alias appear in default_keyboard + `show_in_keyboard`, if True, makes first alias appear + in default_keyboard `descr` is a description `auth` is the lowest authorization level needed to run the command """ - command = command.replace('/','').lower() + command = command.replace('/', '').lower() if not isinstance(command, str): raise TypeError('Command {} is not a string'.format(command)) if aliases: @@ -716,16 +962,37 @@ class Bot(telepot.aio.Bot, Gettable): for alias in aliases: if not isinstance(alias, str): raise TypeError('Alias {} is not a string'.format(alias)) + def decorator(func): if asyncio.iscoroutinefunction(func): async def decorated(message): - logging.info("COMMAND({}) @{} FROM({})".format(command, self.name, message['from'] if 'from' in message else message['chat'])) + logging.info( + "COMMAND({c}) @{n} FROM({f})".format( + c=command, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return await func(message) return self.unauthorized_message else: def decorated(message): - logging.info("COMMAND({}) @{} FROM({})".format(command, self.name, message['from'] if 'from' in message else message['chat'])) + logging.info( + "COMMAND({c}) @{n} FROM({f})".format( + c=command, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return func(message) return self.unauthorized_message @@ -742,25 +1009,54 @@ class Bot(telepot.aio.Bot, Gettable): return decorator def parser(self, condition, descr='', auth='admin', argument='text'): - """ + """Define a message parser. + Decorator: `@bot.parser(condition)` - If condition evaluates True when run on a message text (not starting with '/'), such decorated function gets called on update. - Conditions of parsers are evaluated in order; when one is True, others will be skipped. + If condition evaluates True when run on a message text + (not starting with '/'), such decorated function gets + called on update. + Conditions of parsers are evaluated in order; when one is True, + others will be skipped. `descr` is a description `auth` is the lowest authorization level needed to run the command """ if not callable(condition): - raise TypeError('Condition {} is not a callable'.format(condition.__name__)) + raise TypeError( + 'Condition {} is not a callable'.format( + condition.__name__ + ) + ) + def decorator(func): if asyncio.iscoroutinefunction(func): async def decorated(message): - logging.info("TEXT MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "TEXT MATCHING CONDITION({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return await func(message) return self.unauthorized_message else: def decorated(message): - logging.info("TEXT MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "TEXT MATCHING CONDITION({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return func(message) return self.unauthorized_message @@ -773,28 +1069,56 @@ class Bot(telepot.aio.Bot, Gettable): return decorator def pinned(self, condition, descr='', auth='admin'): - """ + """Handle pinned messages. + Decorator: `@bot.pinned(condition)` - If condition evaluates True when run on a pinned_message update, such decorated function gets called on update. - Conditions are evaluated in order; when one is True, others will be skipped. + If condition evaluates True when run on a pinned_message update, + such decorated function gets called on update. + Conditions are evaluated in order; when one is True, + others will be skipped. `descr` is a description `auth` is the lowest authorization level needed to run the command """ if not callable(condition): - raise TypeError('Condition {} is not a callable'.format(condition.__name__)) + raise TypeError( + 'Condition {c} is not a callable'.format( + c=condition.__name__ + ) + ) + def decorator(func): if asyncio.iscoroutinefunction(func): async def decorated(message): - logging.info("CHAT ACTION MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return await func(message) - return# self.unauthorized_message + return else: def decorated(message): - logging.info("CHAT ACTION MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "PINNED MESSAGE MATCHING({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=( + message['from'] + if 'from' in message + else message['chat'] + ) + ) + ) if self.authorization_function(message, auth): return func(message) - return# self.unauthorized_message + return self.chat_actions['pinned'][condition] = dict( function=decorated, descr=descr, @@ -803,24 +1127,47 @@ class Bot(telepot.aio.Bot, Gettable): return decorator def button(self, data, descr='', auth='admin'): - """ + """Define a bot button. + Decorator: `@bot.button('example:///')` - When a callback data text starts with , it gets passed to the decorated function + When a callback data text starts with , it gets passed to the + decorated function `descr` is a description `auth` is the lowest authorization level needed to run the command """ if not isinstance(data, str): - raise TypeError('Inline button callback_data {} is not a string'.format(data)) + raise TypeError( + 'Inline button callback_data {d} is not a string'.format( + d=data + ) + ) + def decorator(func): if asyncio.iscoroutinefunction(func): async def decorated(message): - logging.info("INLINE BUTTON({}) @{} FROM({})".format(message['data'], self.name, message['from'])) + logging.info( + "INLINE BUTTON({d}) @{n} FROM({f})".format( + d=message['data'], + n=self.name, + f=( + message['from'] + ) + ) + ) if self.authorization_function(message, auth): return await func(message) return self.unauthorized_message else: def decorated(message): - logging.info("INLINE BUTTON({}) @{} FROM({})".format(message['data'], self.name, message['from'])) + logging.info( + "INLINE BUTTON({d}) @{n} FROM({f})".format( + d=message['data'], + n=self.name, + f=( + message['from'] + ) + ) + ) if self.authorization_function(message, auth): return func(message) return self.unauthorized_message @@ -832,24 +1179,44 @@ class Bot(telepot.aio.Bot, Gettable): return decorator def query(self, condition, descr='', auth='admin'): - """ + """Define an inline query. + Decorator: `@bot.query(example)` - When an inline query matches the `condition` function, decorated function is called and passed the query update object as argument. + When an inline query matches the `condition` function, + decorated function is called and passed the query update object + as argument. `descr` is a description `auth` is the lowest authorization level needed to run the command """ if not callable(condition): - raise TypeError('Condition {} is not a callable'.format(condition.__name__)) + raise TypeError( + 'Condition {c} is not a callable'.format( + c=condition.__name__ + ) + ) + def decorator(func): if asyncio.iscoroutinefunction(func): async def decorated(message): - logging.info("QUERY MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "QUERY MATCHING CONDITION({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=message['from'] + ) + ) if self.authorization_function(message, auth): return await func(message) return self.unauthorized_message else: def decorated(message): - logging.info("QUERY MATCHING CONDITION({}) @{} FROM({})".format(condition.__name__, self.name, message['from'])) + logging.info( + "QUERY MATCHING CONDITION({c}) @{n} FROM({f})".format( + c=condition.__name__, + n=self.name, + f=message['from'] + ) + ) if self.authorization_function(message, auth): return func(message) return self.unauthorized_message @@ -861,8 +1228,13 @@ class Bot(telepot.aio.Bot, Gettable): return decorator def additional_task(self, when='BEFORE'): - """Decorator: such decorated async functions get awaited BEFORE or AFTER messageloop""" + """Add a task before or after message_loop. + + Decorator: such decorated async functions get awaited BEFORE or + AFTER messageloop + """ when = when[0].lower() + def decorator(func): if when == 'b': self.run_before_loop.append(func()) @@ -872,9 +1244,11 @@ class Bot(telepot.aio.Bot, Gettable): def set_default_keyboard(self, keyboard='set_default'): """Set a default keyboard for the bot. - If a keyboard is not passed as argument, a default one is generated, based on aliases of commands. + + If a keyboard is not passed as argument, a default one is generated, + based on aliases of commands. """ - if keyboard=='set_default': + if keyboard == 'set_default': btns = [ dict( text=x @@ -895,7 +1269,9 @@ class Bot(telepot.aio.Bot, Gettable): async def edit_message(self, update, *args, **kwargs): """Edit given update with given *args and **kwargs. - Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards. + + Please note, that it is currently only possible to edit messages + without reply_markup or with inline keyboards. """ try: return await self.editMessageText( @@ -908,7 +1284,9 @@ class Bot(telepot.aio.Bot, Gettable): async def delete_message(self, update, *args, **kwargs): """Delete given update with given *args and **kwargs. - Please note, that a bot can delete only messages sent by itself or sent in a group which it is administrator of. + + Please note, that a bot can delete only messages sent by itself + or sent in a group which it is administrator of. """ try: return await self.deleteMessage( @@ -919,22 +1297,33 @@ class Bot(telepot.aio.Bot, Gettable): except Exception as e: logging.error("{}".format(e)) - async def send_message(self, answer=dict(), chat_id=None, text='', parse_mode="HTML", disable_web_page_preview=None, disable_notification=None, reply_to_message_id=None, reply_markup=None): - """Convenient method to call telepot.Bot(token).sendMessage - All sendMessage **kwargs can be either **kwargs of send_message or key:val of answer argument. + async def send_message(self, answer=dict(), chat_id=None, text='', + parse_mode="HTML", disable_web_page_preview=None, + disable_notification=None, reply_to_message_id=None, + reply_markup=None): + """Send a message. + + Convenient method to call telepot.Bot(token).sendMessage + All sendMessage **kwargs can be either **kwargs of send_message + or key:val of answer argument. Messages longer than telegram limit will be split properly. - Telegram flood limits won't be reached thanks to `await avoid_flooding(chat_id)` + Telegram flood limits won't be reached thanks to + `await avoid_flooding(chat_id)` parse_mode will be checked and edited if necessary. Arguments will be checked and adapted. """ if type(answer) is dict and 'chat_id' in answer: chat_id = answer['chat_id'] - # chat_id may simply be the update to which the bot should repy: get_chat_id is called + # chat_id may simply be the update to which the bot should repy if type(chat_id) is dict: chat_id = self.get_chat_id(chat_id) if type(answer) is str: text = answer - if not reply_markup and chat_id > 0 and text!=self.unauthorized_message: + if ( + not reply_markup + and chat_id > 0 + and text != self.unauthorized_message + ): reply_markup = self.default_keyboard elif type(answer) is dict: if 'text' in answer: @@ -949,7 +1338,12 @@ class Bot(telepot.aio.Bot, Gettable): reply_to_message_id = answer['reply_to_message_id'] if 'reply_markup' in answer: reply_markup = answer['reply_markup'] - elif not reply_markup and type(chat_id) is int and chat_id > 0 and text!=self.unauthorized_message: + elif ( + not reply_markup + and type(chat_id) is int + and chat_id > 0 + and text != self.unauthorized_message + ): reply_markup = self.default_keyboard assert type(text) is str, "Text is not a string!" assert ( @@ -966,7 +1360,7 @@ class Bot(telepot.aio.Bot, Gettable): ) n = len(text_chunks) for text_chunk in text_chunks: - n-=1 + n -= 1 if parse_mode.lower() == "html": this_parse_mode = "HTML" # Check that all tags are well-formed @@ -979,7 +1373,10 @@ class Bot(telepot.aio.Bot, Gettable): ] ): this_parse_mode = "None" - text_chunk = "!!![invalid markdown syntax]!!!\n\n" + text_chunk + text_chunk = ( + "!!![invalid markdown syntax]!!!\n\n" + + text_chunk + ) elif parse_mode != "None": this_parse_mode = "Markdown" # Check that all markdowns are well-formed @@ -990,10 +1387,13 @@ class Bot(telepot.aio.Bot, Gettable): ] ): this_parse_mode = "None" - text_chunk = "!!![invalid markdown syntax]!!!\n\n" + text_chunk + text_chunk = ( + "!!![invalid markdown syntax]!!!\n\n" + + text_chunk + ) else: this_parse_mode = parse_mode - this_reply_markup = reply_markup if n==0 else None + this_reply_markup = reply_markup if n == 0 else None try: await self.avoid_flooding(chat_id) result = await self.sendMessage( @@ -1006,23 +1406,38 @@ class Bot(telepot.aio.Bot, Gettable): reply_markup=this_reply_markup ) except Exception as e: - logging.debug(e, exc_info=False) # Set exc_info=True for more information + logging.debug( + e, + exc_info=False # Set exc_info=True for more information + ) result = e return result - async def send_photo(self, chat_id=None, answer={}, photo=None, caption='', parse_mode='HTML', disable_notification=None, reply_to_message_id=None,reply_markup=None, use_stored=True, second_chance=False): - """Convenient method to call telepot.Bot(token).sendPhoto - All sendPhoto **kwargs can be either **kwargs of send_message or key:val of answer argument. + async def send_photo(self, chat_id=None, answer={}, + photo=None, caption='', parse_mode='HTML', + disable_notification=None, reply_to_message_id=None, + reply_markup=None, use_stored=True, + second_chance=False): + """Send a photo. + + Convenient method to call telepot.Bot(token).sendPhoto + All sendPhoto **kwargs can be either **kwargs of send_message + or key:val of answer argument. Captions longer than telegram limit will be shortened gently. - Telegram flood limits won't be reached thanks to `await avoid_flooding(chat_id)` + Telegram flood limits won't be reached thanks to + `await avoid_flooding(chat_id)` Most arguments will be checked and adapted. - If use_stored is set to True, the bot will store sent photo telegram_id and use it for faster sending next times (unless future errors). - Sending photos by their file_id already stored on telegram servers is way faster: that's why bot stores and uses this info, if required. + If use_stored is set to True, the bot will store sent photo + telegram_id and use it for faster sending next times (unless + future errors). + Sending photos by their file_id already stored on telegram servers + is way faster: that's why bot stores and uses this info, + if required. A second_chance is given to send photo on error. """ if 'chat_id' in answer: chat_id = answer['chat_id'] - # chat_id may simply be the update to which the bot should repy: get_chat_id is called + # chat_id may simply be the update to which the bot should repy if type(chat_id) is dict: chat_id = self.get_chat_id(chat_id) assert ( @@ -1046,7 +1461,10 @@ class Bot(telepot.aio.Bot, Gettable): if type(photo) is str: photo_url = photo with self.db as db: - already_sent = db['sent_pictures'].find_one(url=photo_url, errors=False) + already_sent = db['sent_pictures'].find_one( + url=photo_url, + errors=False + ) if already_sent and use_stored: photo = already_sent['file_id'] already_sent = True @@ -1054,7 +1472,13 @@ class Bot(telepot.aio.Bot, Gettable): already_sent = False if not any(photo_url.startswith(x) for x in ['http', 'www']): with io.BytesIO() as buffered_picture: - with open("{}/{}".format(self.path, photo_url), 'rb') as photo_file: + with open( + "{}/{}".format( + self.path, + photo_url + ), + 'rb' + ) as photo_file: buffered_picture.write(photo_file.read()) photo = buffered_picture.getvalue() caption = escape_html_chars(caption) @@ -1094,7 +1518,12 @@ class Bot(telepot.aio.Bot, Gettable): if isinstance(sent, Exception): raise Exception("SendingFailed") except Exception as e: - logging.error("Error sending photo\n{}".format(e), exc_info=False) # Set exc_info=True for more information + logging.error( + "Error sending photo\n{}".format( + e + ), + exc_info=False # Set exc_info=True for more information + ) if already_sent: with self.db as db: db['sent_pictures'].update( @@ -1121,7 +1550,7 @@ class Bot(telepot.aio.Bot, Gettable): sent is not None and hasattr(sent, '__getitem__') and 'photo' in sent - and len(sent['photo'])>0 + and len(sent['photo']) > 0 and 'file_id' in sent['photo'][0] and (not already_sent) and use_stored @@ -1136,8 +1565,9 @@ class Bot(telepot.aio.Bot, Gettable): ) return sent - async def send_and_destroy(self, chat_id, answer, timer=60, mode='text', **kwargs): - """Send a message or photo and delete it after `timer` seconds""" + async def send_and_destroy(self, chat_id, answer, + timer=60, mode='text', **kwargs): + """Send a message or photo and delete it after `timer` seconds.""" if mode == 'text': sent_message = await self.send_message( chat_id=chat_id, @@ -1159,37 +1589,65 @@ class Bot(telepot.aio.Bot, Gettable): return async def wait_and_obscure(self, update, when, inline_message_id): - """Obscure an inline_message `timer` seconds after sending it, by editing its text or caption. - At the moment Telegram won't let bots delete sent inline query results.""" + """Obscure messages which can't be deleted. + + Obscure an inline_message `timer` seconds after sending it, + by editing its text or caption. + At the moment Telegram won't let bots delete sent inline query results. + """ if type(when) is int: when = datetime.datetime.now() + datetime.timedelta(seconds=when) - assert type(when) is datetime.datetime, "when must be a datetime instance or a number of seconds (int) to be awaited" + assert type(when) is datetime.datetime, "when must be a datetime "\ + "instance or a number of "\ + "seconds (int) to be awaited" if 'inline_message_id' not in update: - logging.info("This inline query result owns no inline_keyboard, so it can't be modified") + logging.info( + "This inline query result owns no inline_keyboard, so it " + "can't be modified" + ) return inline_message_id = update['inline_message_id'] self.to_be_obscured.append(inline_message_id) while datetime.datetime.now() < when: await sleep_until(when) try: - await self.editMessageCaption(inline_message_id, text="Time over") - except: + await self.editMessageCaption( + inline_message_id, + text="Time over" + ) + except Exception: try: - await self.editMessageText(inline_message_id, text="Time over") + await self.editMessageText( + inline_message_id, + text="Time over" + ) except Exception as e: - logging.error("Couldn't obscure message\n{}\n\n{}".format(inline_message_id,e)) + logging.error( + "Couldn't obscure message\n{}\n\n{}".format( + inline_message_id, + e + ) + ) self.to_be_obscured.remove(inline_message_id) return async def continue_running(self): - """If bot can be got, sets name and telegram_id, awaits preliminary tasks and starts getting updates from telegram. - If bot can't be got, restarts all bots in 5 minutes.""" + """Get updates. + + If bot can be got, sets name and telegram_id, + awaits preliminary tasks and starts getting updates from telegram. + If bot can't be got, restarts all bots in 5 minutes. + """ try: me = await self.getMe() self.bot_name = me["username"] self.telegram_id = me['id'] - except: - logging.error("Could not get bot") + except Exception as e: + logging.error( + "Could not get bot\n{e}".format( + e=e + ) + ) await asyncio.sleep(5*60) self.restart_bots() return @@ -1202,17 +1660,20 @@ class Bot(telepot.aio.Bot, Gettable): return def stop_bots(self): - """Causes the script to exit""" + """Exit script with code 0.""" Bot.stop = True def restart_bots(self): - """Causes the script to restart. - Actually, you need to catch Bot.stop state when Bot.run() returns and handle the situation yourself.""" + """Restart the script exiting with code 65. + + Actually, you need to catch Bot.stop state when Bot.run() returns + and handle the situation yourself. + """ Bot.stop = "Restart" @classmethod async def check_task(cls): - """Await until cls.stop, then end session and return""" + """Await until cls.stop, then end session and return.""" for bot in cls.instances.values(): asyncio.ensure_future(bot.continue_running()) while not cls.stop: @@ -1222,7 +1683,10 @@ class Bot(telepot.aio.Bot, Gettable): @classmethod async def end_session(cls): """Run after stop, before the script exits. - Await final tasks, obscure and delete pending messages, log current operation (stop/restart).""" + + Await final tasks, obscure and delete pending messages, + log current operation (stop/restart). + """ for bot in cls.instances.values(): for task in bot.run_after_loop: await task @@ -1230,18 +1694,34 @@ class Bot(telepot.aio.Bot, Gettable): try: await bot.delete_message(message) except Exception as e: - logging.error("Couldn't delete message\n{}\n\n{}".format(message,e)) + logging.error( + "Couldn't delete message\n{}\n\n{}".format( + message, + e + ) + ) for inline_message_id in bot.to_be_obscured: try: - await bot.editMessageCaption(inline_message_id, text="Time over") - except: + await bot.editMessageCaption( + inline_message_id, + text="Time over" + ) + except Exception: try: - await bot.editMessageText(inline_message_id, text="Time over") + await bot.editMessageText( + inline_message_id, + text="Time over" + ) except Exception as e: - logging.error("Couldn't obscure message\n{}\n\n{}".format(inline_message_id,e)) - if cls.stop=="Restart": + logging.error( + "Couldn't obscure message\n{}\n\n{}".format( + inline_message_id, + e + ) + ) + if cls.stop == "Restart": logging.info("\n\t\t---Restart!---") - elif cls.stop=="KeyboardInterrupt": + elif cls.stop == "KeyboardInterrupt": logging.info("Stopped by KeyboardInterrupt.") else: logging.info("Stopped gracefully by user.") @@ -1249,31 +1729,38 @@ class Bot(telepot.aio.Bot, Gettable): @classmethod def run(cls, loop=None): - """ - Call this method to run the async bots. - """ + """Call this method to run the async bots.""" if not loop: loop = asyncio.get_event_loop() logging.info( "{sep}{subjvb} STARTED{sep}".format( sep='-'*10, - subjvb='BOT HAS' if len(cls.instances)==1 else 'BOTS HAVE' + subjvb='BOT HAS' if len(cls.instances) == 1 else 'BOTS HAVE' ) ) try: loop.run_until_complete(cls.check_task()) except KeyboardInterrupt: - logging.info('\n\t\tYour script received a KeyboardInterrupt signal, your bot{} being stopped.'.format( - 's are' if len(cls.instances)>1 else ' is' - )) + logging.info( + '\n\t\tYour script received a KeyboardInterrupt signal, "\ + "your bot{} being stopped.'.format( + 's are' if len(cls.instances) > 1 else ' is' + ) + ) + cls.stop = "KeyboardInterrupt" loop.run_until_complete(cls.end_session()) except Exception as e: - logging.error('\nYour bot has been stopped. with error \'{}\''.format(e), exc_info=True) + logging.error( + '\nYour bot has been stopped. with error \'{}\''.format( + e + ), + exc_info=True + ) logging.info( "{sep}{subjvb} STOPPED{sep}".format( sep='-'*10, - subjvb='BOT HAS' if len(cls.instances)==1 else 'BOTS HAVE' + subjvb='BOT HAS' if len(cls.instances) == 1 else 'BOTS HAVE' ) ) return