Queer European MD passionate about IT
Browse Source

/log and /errors commands implemented

Receive bot general log and errors log via bot itself
Davte 5 years ago
parent
commit
417dcb63e4
3 changed files with 247 additions and 2 deletions
  1. 1 1
      davtelepot/__init__.py
  2. 191 1
      davtelepot/administration_tools.py
  3. 55 0
      davtelepot/bot.py

+ 1 - 1
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"
 

+ 191 - 1
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"
+                  "<b>Error:</b>\n"
+                  "<code>{e}</code>",
+            'it': "Inviio del messaggio di log fallito!\n\n"
+                  "<b>Errore:</b>\n"
+                  "<code>{e}</code>"
+        },
+        '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"
+                  "<b>Error:</b>\n"
+                  "<code>{e}</code>",
+            'it': "Inviio del messaggio di log degli errori fallito!\n\n"
+                  "<b>Errore:</b>\n"
+                  "<code>{e}</code>"
+        },
+        '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)

+ 55 - 0
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.