diff --git a/davtelepot/__init__.py b/davtelepot/__init__.py index fb0728f..367e13a 100644 --- a/davtelepot/__init__.py +++ b/davtelepot/__init__.py @@ -14,7 +14,7 @@ __author__ = "Davide Testa" __email__ = "davide@davte.it" __credits__ = ["Marco Origlia", "Nick Lee @Nickoala"] __license__ = "GNU General Public License v3.0" -__version__ = "2.1.34" +__version__ = "2.1.35" __maintainer__ = "Davide Testa" __contact__ = "t.me/davte" diff --git a/davtelepot/administration_tools.py b/davtelepot/administration_tools.py index c70db1c..e975650 100644 --- a/davtelepot/administration_tools.py +++ b/davtelepot/administration_tools.py @@ -17,7 +17,8 @@ import json from davtelepot.utilities import ( async_wrapper, Confirmator, extract, get_cleaned_text, get_user, escape_html_chars, line_drawing_unordered_list, make_button, - make_inline_keyboard, remove_html_tags, send_csv_file + make_inline_keyboard, remove_html_tags, send_part_of_text_file, + send_csv_file ) from sqlalchemy.exc import ResourceClosedError @@ -624,6 +625,86 @@ default_admin_messages = { 'en': "No result to show.", 'it': "Nessun risultato da mostrare." } + }, + 'log_command': { + 'description': { + 'en': "Receive bot log file, if set", + 'it': "Ricevi il file di log del bot, se impostato" + }, + 'no_log': { + 'en': "Sorry but no log file is set.\n" + "To set it, use `bot.set_log_file_name` instance method or " + "`Bot.set_class_log_file_name` class method.", + 'it': "Spiacente ma il file di log non è stato impostato.\n" + "Per impostarlo, usa il metodo d'istanza " + "`bot.set_log_file_name` o il metodo di classe" + "`Bot.set_class_log_file_name`." + }, + 'sending_failure': { + 'en': "Sending log file failed!\n\n" + "Error:\n" + "{e}", + 'it': "Inviio del messaggio di log fallito!\n\n" + "Errore:\n" + "{e}" + }, + 'here_is_log_file': { + 'en': "Here is the complete log file.", + 'it': "Ecco il file di log completo." + }, + 'log_file_first_lines': { + 'en': "Here are the first {lines} lines of the log file.", + 'it': "Ecco le prime {lines} righe del file di log." + }, + 'log_file_last_lines': { + 'en': "Here are the last {lines} lines of the log file.\n" + "Newer lines are at the top of the file.", + 'it': "Ecco le ultime {lines} righe del file di log.\n" + "L'ordine è cronologico, con i messaggi nuovi in alto." + } + }, + 'errors_command': { + 'description': { + 'en': "Receive bot error log file, if set", + 'it': "Ricevi il file di log degli errori del bot, se impostato" + }, + 'no_log': { + 'en': "Sorry but no errors log file is set.\n" + "To set it, use `bot.set_errors_file_name` instance method" + "or `Bot.set_class_errors_file_name` class method.", + 'it': "Spiacente ma il file di log degli errori non è stato " + "impostato.\n" + "Per impostarlo, usa il metodo d'istanza " + "`bot.set_errors_file_name` o il metodo di classe" + "`Bot.set_class_errors_file_name`." + }, + 'empty_log': { + 'en': "Congratulations! Errors log is empty!", + 'it': "Congratulazioni! Il log degli errori è vuoto!" + }, + 'sending_failure': { + 'en': "Sending errors log file failed!\n\n" + "Error:\n" + "{e}", + 'it': "Inviio del messaggio di log degli errori fallito!\n\n" + "Errore:\n" + "{e}" + }, + 'here_is_log_file': { + 'en': "Here is the complete errors log file.", + 'it': "Ecco il file di log degli errori completo." + }, + 'log_file_first_lines': { + 'en': "Here are the first {lines} lines of the errors log file.", + 'it': "Ecco le prime {lines} righe del file di log degli errori." + }, + 'log_file_last_lines': { + 'en': "Here are the last {lines} lines of the errors log file.\n" + "Newer lines are at the top of the file.", + 'it': "Ecco le ultime {lines} righe del file di log degli " + "errori.\n" + "L'ordine è cronologico, con i messaggi nuovi in alto." + } } } @@ -867,6 +948,103 @@ async def _query_button(bot, update, user_record, data): return result +async def _log_command(bot, update, user_record): + if bot.log_file_path is None: + return bot.get_message( + 'admin', 'log_command', 'no_log', + update=update, user_record=user_record + ) + # Always send log file in private chat + chat_id = update['from']['id'] + text = get_cleaned_text(update, bot, ['log']) + reversed_ = 'r' not in text + text = text.strip('r') + if text.isnumeric(): + limit = int(text) + else: + limit = 100 + if limit is None: + sent = await bot.send_document( + chat_id=chat_id, + document_path=bot.log_file_path, + caption=bot.get_message( + 'admin', 'log_command', 'here_is_log_file', + update=update, user_record=user_record + ) + ) + else: + sent = await send_part_of_text_file( + bot=bot, + update=update, + user_record=user_record, + chat_id=chat_id, + file_path=bot.log_file_path, + file_name=bot.log_file_name, + caption=bot.get_message( + 'admin', 'log_command', ( + 'log_file_last_lines' + if reversed_ + else 'log_file_first_lines' + ), + update=update, user_record=user_record, + lines=limit + ), + reversed_=reversed_, + limit=limit + ) + if isinstance(sent, Exception): + return bot.get_message( + 'admin', 'log_command', 'sending_failure', + update=update, user_record=user_record, + e=sent + ) + return + + +async def _errors_command(bot, update, user_record): + # Always send errors log file in private chat + chat_id = update['from']['id'] + if bot.errors_file_path is None: + return bot.get_message( + 'admin', 'errors_command', 'no_log', + update=update, user_record=user_record + ) + await bot.sendChatAction(chat_id=chat_id, action='upload_document') + try: + # Check that error log is not empty + with open(bot.errors_file_path, 'r') as errors_file: + for line in errors_file: + break + else: + return bot.get_message( + 'admin', 'errors_command', 'empty_log', + update=update, user_record=user_record + ) + # Send error log + sent = await bot.send_document( + # Always send log file in private chat + chat_id=chat_id, + document_path=bot.errors_file_path, + caption=bot.get_message( + 'admin', 'errors_command', 'here_is_log_file', + update=update, user_record=user_record + ) + ) + # Reset error log + with open(bot.errors_file_path, 'w') as errors_file: + errors_file.write('') + except Exception as e: + sent = e + # Notify failure + if isinstance(sent, Exception): + return bot.get_message( + 'admin', 'errors_command', 'sending_failure', + update=update, user_record=user_record, + e=sent + ) + return + + def init(bot, talk_messages=None, admin_messages=None): """Assign parsers, commands, buttons and queries to given `bot`.""" if talk_messages is None: @@ -992,3 +1170,15 @@ def init(bot, talk_messages=None, admin_messages=None): authorization_level='admin') async def query_button(bot, update, user_record, data): return await _query_button(bot, update, user_record, data) + + @bot.command(command='/log', aliases=[], show_in_keyboard=False, + description=admin_messages['log_command']['description'], + authorization_level='admin') + async def log_command(bot, update, user_record): + return await _log_command(bot, update, user_record) + + @bot.command(command='/errors', aliases=[], show_in_keyboard=False, + description=admin_messages['errors_command']['description'], + authorization_level='admin') + async def errors_command(bot, update, user_record): + return await _errors_command(bot, update, user_record) diff --git a/davtelepot/bot.py b/davtelepot/bot.py index da10d95..d426e02 100644 --- a/davtelepot/bot.py +++ b/davtelepot/bot.py @@ -86,6 +86,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): ) ) ] + _log_file_name = None + _errors_file_name = None def __init__( self, token, hostname='', certificate=None, max_connections=40, @@ -206,6 +208,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): self.default_reply_keyboard_elements = [] self._default_keyboard = dict() self.recent_users = OrderedDict() + self._log_file_name = None + self._errors_file_name = None return @property @@ -225,6 +229,57 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): """Set instance path attribute.""" self._path = path + @property + def log_file_name(self): + """Return log file name. + + Fallback to class file name if set, otherwise return None. + """ + return self._log_file_name or self.__class__._log_file_name + + @property + def log_file_path(self): + """Return log file path basing on self.path and `_log_file_name`. + + Fallback to class file if set, otherwise return None. + """ + return f"{self.path}/data/{self.log_file_name}" + + def set_log_file_name(self, file_name): + """Set log file name.""" + self._log_file_name = file_name + + @classmethod + def set_class_log_file_name(cls, file_name): + """Set class log file name.""" + cls._log_file_name = file_name + + @property + def errors_file_name(self): + """Return errors file name. + + Fallback to class file name if set, otherwise return None. + """ + return self._errors_file_name or self.__class__._errors_file_name + + @property + def errors_file_path(self): + """Return errors file path basing on self.path and `_errors_file_name`. + + Fallback to class file if set, otherwise return None. + """ + if self.errors_file_name: + return f"{self.path}/data/{self.errors_file_name}" + + def set_errors_file_name(self, file_name): + """Set errors file name.""" + self._errors_file_name = file_name + + @classmethod + def set_class_errors_file_name(cls, file_name): + """Set class errors file name.""" + cls._errors_file_name = file_name + @classmethod def get(cls, token, *args, **kwargs): """Given a `token`, return class instance with that token.