From edb22017736c1806aa753c176a3b8132ab650c85 Mon Sep 17 00:00:00 2001 From: Davte Date: Mon, 27 Apr 2020 13:48:33 +0200 Subject: [PATCH] Package updates checker implemented Notify administrators when new versions are available for PyPi packages in `bot.packages`. --- davtelepot/__init__.py | 2 +- davtelepot/administration_tools.py | 152 ++++++++++++++++++++--------- davtelepot/api.py | 8 +- davtelepot/authorization.py | 98 ++++--------------- davtelepot/bot.py | 28 +++++- davtelepot/messages.py | 91 +++++++++++++++-- davtelepot/suggestions.py | 17 +--- setup.py | 1 + 8 files changed, 247 insertions(+), 150 deletions(-) diff --git a/davtelepot/__init__.py b/davtelepot/__init__.py index 4ebc502..0c4e12f 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.4.25" +__version__ = "2.5.0" __maintainer__ = "Davide Testa" __contact__ = "t.me/davte" diff --git a/davtelepot/administration_tools.py b/davtelepot/administration_tools.py index 60bc14c..e8287a1 100644 --- a/davtelepot/administration_tools.py +++ b/davtelepot/administration_tools.py @@ -12,17 +12,19 @@ davtelepot.admin_tools.init(my_bot) import asyncio import datetime import json +import logging # Third party modules -import davtelepot -from davtelepot import messages -from davtelepot.utilities import ( - async_wrapper, Confirmator, extract, get_cleaned_text, get_user, - escape_html_chars, line_drawing_unordered_list, make_button, +from sqlalchemy.exc import ResourceClosedError + +# Project modules +from . import bot as davtelepot_bot, messages, __version__ +from .utilities import ( + async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text, + get_user, escape_html_chars, line_drawing_unordered_list, make_button, make_inline_keyboard, remove_html_tags, send_part_of_text_file, send_csv_file ) -from sqlalchemy.exc import ResourceClosedError async def _forward_to(update, bot, sender, addressee, is_admin=False): @@ -179,11 +181,9 @@ def get_talk_panel(bot, update, user_record=None, text=''): { key: val for key, val in user.items() - if key in ( - 'first_name', - 'last_name', - 'username' - ) + if key in ('first_name', + 'last_name', + 'username') } ) ), @@ -779,7 +779,7 @@ def get_maintenance_exception_criterion(bot, allowed_command): return criterion -async def get_version(): +async def get_last_commit(): """Get last commit hash and davtelepot version.""" try: _subprocess = await asyncio.create_subprocess_exec( @@ -793,70 +793,132 @@ async def get_version(): last_commit = f"{e}" if last_commit.startswith("fatal: not a git repository"): last_commit = "-" - davtelepot_version = davtelepot.__version__ - return last_commit, davtelepot_version + return last_commit async def _version_command(bot, update, user_record): - last_commit, davtelepot_version = await get_version() + last_commit = await get_last_commit() return bot.get_message( 'admin', 'version_command', 'result', last_commit=last_commit, - davtelepot_version=davtelepot_version, + davtelepot_version=__version__, update=update, user_record=user_record ) -async def notify_new_version(bot): +async def notify_new_version(bot: davtelepot_bot): """Notify `bot` administrators about new versions. Notify admins when last commit and/or davtelepot version change. """ - last_commit, davtelepot_version = await get_version() + last_commit = await get_last_commit() old_record = bot.db['version_history'].find_one( order_by=['-id'] ) + current_versions = { + f"{package.__name__}_version": package.__version__ + for package in bot.packages + } + current_versions['last_commit'] = last_commit if old_record is None: old_record = dict( updated_at=datetime.datetime.min, - last_commit=None, - davtelepot_version=None ) - if ( - old_record['last_commit'] != last_commit - or old_record['davtelepot_version'] != davtelepot_version + for name in current_versions.keys(): + if name not in old_record: + old_record[name] = None + if any( + old_record[name] != current_version + for name, current_version in current_versions.items() ): - new_record = dict( - updated_at=datetime.datetime.now(), - last_commit=last_commit, - davtelepot_version=davtelepot_version - ) bot.db['version_history'].insert( - new_record + dict( + updated_at=datetime.datetime.now(), + **current_versions + ) ) - for admin in bot.db['users'].find(privileges=[1, 2]): + for admin in bot.administrators: + text = bot.get_message( + 'admin', 'new_version', 'title', + user_record=admin + ) + '\n\n' + if last_commit != old_record['last_commit']: + text += bot.get_message( + 'admin', 'new_version', 'last_commit', + old_record=old_record, + new_record=current_versions, + user_record=admin + ) + '\n\n' + text += '\n'.join( + f"{name[:-len('_version')]}: " + f"{old_record[name]} —> " + f"{current_version}" + for name, current_version in current_versions.items() + if name not in ('last_commit', ) + and current_version != old_record[name] + ) await bot.send_message( chat_id=admin['telegram_id'], disable_notification=True, - text='\n\n'.join( - bot.get_message( - 'admin', 'new_version', field, - old_record=old_record, - new_record=new_record, - user_record=admin - ) - for field in filter( - lambda x: (x not in old_record - or old_record[x] != new_record[x]), - ('title', 'last_commit', 'davtelepot_version') - ) - ) + text=text ) return -def init(telegram_bot, talk_messages=None, admin_messages=None): +async def get_package_updates(bot: davtelepot_bot, + monitoring_interval: int = 60 * 60): + while 1: + news = dict() + for package in bot.packages: + package_web_page = CachedPage.get( + f'https://pypi.python.org/pypi/{package.__name__}/json', + cache_time=2, + mode='json' + ) + web_page = await package_web_page.get_page() + if web_page is None or isinstance(web_page, Exception): + logging.error(f"Cannot get updates for {package.__name__}, " + "skipping...") + continue + new_version = web_page['info']['version'] + current_version = package.__version__ + if new_version != current_version: + news[package.__name__] = { + 'current': current_version, + 'new': new_version + } + if news: + for admin in bot.administrators: + text = bot.get_message( + 'admin', 'updates_available', 'header', + user_record=admin + ) + '\n\n' + text += '\n'.join( + f"{package}: " + f"{versions['current']} —> " + f"{versions['new']}" + for package, versions in news.items() + ) + await bot.send_message( + chat_id=admin['telegram_id'], + disable_notification=True, + text=text + ) + await asyncio.sleep(monitoring_interval) + + +def init(telegram_bot, + talk_messages=None, + admin_messages=None, + packages=None): """Assign parsers, commands, buttons and queries to given `bot`.""" + if packages is None: + packages = [] + telegram_bot.packages.extend( + filter(lambda package: package not in telegram_bot.packages, + packages) + ) + asyncio.ensure_future(get_package_updates(telegram_bot)) if talk_messages is None: talk_messages = messages.default_talk_messages telegram_bot.messages['talk'] = talk_messages @@ -1045,7 +1107,7 @@ def init(telegram_bot, talk_messages=None, admin_messages=None): 'help_section',) }, show_in_keyboard=False, - authorization_level='admin',) + authorization_level='admin') async def version_command(bot, update, user_record): return await _version_command(bot=bot, update=update, diff --git a/davtelepot/api.py b/davtelepot/api.py index 23226d8..c265f0b 100644 --- a/davtelepot/api.py +++ b/davtelepot/api.py @@ -1,4 +1,4 @@ -"""This module provides a glow-like middleware for Telegram bot API. +"""This module provides a python mirror for Telegram bot API. All methods and parameters are the same as the original json API. A simple aiohttp asynchronous web client is used to make requests. @@ -10,11 +10,11 @@ import datetime import json import logging -# Third party modules from typing import Union, List +# Third party modules import aiohttp -from aiohttp import web +import aiohttp.web class TelegramError(Exception): @@ -82,7 +82,7 @@ class TelegramBot: """ loop = asyncio.get_event_loop() - app = web.Application() + app = aiohttp.web.Application() sessions_timeouts = { 'getUpdates': dict( timeout=35, diff --git a/davtelepot/authorization.py b/davtelepot/authorization.py index 4738e33..eea4b4f 100644 --- a/davtelepot/authorization.py +++ b/davtelepot/authorization.py @@ -2,9 +2,11 @@ # Standard library modules from collections import OrderedDict +from typing import Callable, Union # Project modules from .bot import Bot +from .messages import default_authorization_messages from .utilities import ( Confirmator, get_cleaned_text, get_user, make_button, make_inline_keyboard ) @@ -256,85 +258,10 @@ def get_authorization_function(bot): return is_authorized -deafult_authorization_messages = { - 'auth_command': { - 'description': { - 'en': "Edit user permissions. To select a user, reply to " - "a message of theirs or write their username", - 'it': "Cambia il grado di autorizzazione di un utente " - "(in risposta o scrivendone lo username)" - }, - 'unhandled_case': { - 'en': "Unhandled case :/", - 'it': "Caso non previsto :/" - }, - 'instructions': { - 'en': "Reply with this command to a user or write " - "/auth username to edit their permissions.", - 'it': "Usa questo comando in risposta a un utente " - "oppure scrivi /auth username per " - "cambiarne il grado di autorizzazione." - }, - 'unknown_user': { - 'en': "Unknown user.", - 'it': "Utente sconosciuto." - }, - 'choose_user': { - 'en': "{n} users match your query. Please select one.", - 'it': "Ho trovato {n} utenti che soddisfano questi criteri.\n" - "Per procedere selezionane uno." - }, - 'no_match': { - 'en': "No user matches your query. Please try again.", - 'it': "Non ho trovato utenti che soddisfino questi criteri.\n" - "Prova di nuovo." - } - }, - 'ban_command': { - 'description': { - 'en': "Reply to a user with /ban to ban them", - 'it': "Banna l'utente (da usare in risposta)" - } - }, - 'auth_button': { - 'description': { - 'en': "Edit user permissions", - 'it': "Cambia il grado di autorizzazione di un utente" - }, - 'confirm': { - 'en': "Are you sure?", - 'it': "Sicuro sicuro?" - }, - 'back_to_user': { - 'en': "Back to user", - 'it': "Torna all'utente" - }, - 'permission_denied': { - 'user': { - 'en': "You cannot appoint this user!", - 'it': "Non hai l'autorità di modificare i permessi di questo " - "utente!" - }, - 'role': { - 'en': "You're not allowed to appoint someone to this role!", - 'it': "Non hai l'autorità di conferire questo permesso!" - } - }, - 'no_change': { - 'en': "No change suggested!", - 'it': "È già così!" - }, - 'appointed': { - 'en': "Permission granted", - 'it': "Permesso conferito" - } - }, -} - - async def _authorization_command(bot, update, user_record): text = get_cleaned_text(bot=bot, update=update, replace=['auth']) reply_markup = None + # noinspection PyUnusedLocal result = bot.get_message( 'authorization', 'auth_command', 'unhandled_case', update=update, user_record=user_record @@ -509,7 +436,17 @@ async def _ban_command(bot, update, user_record): return -def init(telegram_bot: Bot, roles=None, authorization_messages=None): +def default_get_administrators_function(bot: Bot): + return list( + bot.db['users'].find(privileges=[1,2]) + ) + + +def init(telegram_bot: Bot, + roles: Union[list, OrderedDict] = None, + authorization_messages=None, + get_administrators_function: Callable[[object], + list] = None): """Set bot roles and assign role-related commands. Pass an OrderedDict of `roles` to get them set. @@ -537,8 +474,11 @@ def init(telegram_bot: Bot, roles=None, authorization_messages=None): telegram_bot.set_authorization_function( get_authorization_function(telegram_bot) ) - if authorization_messages is None: - authorization_messages = deafult_authorization_messages + get_administrators_function = (get_administrators_function + or default_get_administrators_function) + telegram_bot.set_get_administrator_function(get_administrators_function) + authorization_messages = (authorization_messages + or default_authorization_messages) telegram_bot.messages['authorization'] = authorization_messages @telegram_bot.command(command='/auth', aliases=[], show_in_keyboard=False, diff --git a/davtelepot/bot.py b/davtelepot/bot.py index 5bb9b74..742f074 100644 --- a/davtelepot/bot.py +++ b/davtelepot/bot.py @@ -34,13 +34,16 @@ Usage # Standard library modules import asyncio -from collections import OrderedDict import datetime import io import inspect import logging import os import re +import sys + +from collections import OrderedDict +from typing import Callable # Third party modules from aiohttp import web @@ -210,6 +213,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): if 'chat' in update else None ) + # Function to get updated list of bot administrators + self._get_administrators = lambda bot: [] # Message to be returned if user is not allowed to call method self._authorization_denied_message = None # Default authorization function (always return True) @@ -223,6 +228,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): self.placeholder_requests = dict() self.shared_data = dict() self.Role = None + self.packages = [sys.modules['davtelepot']] # Add `users` table with its fields if missing if 'users' not in self.db.tables: table = self.db.create_table( @@ -553,6 +559,26 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): default_inline_query_answer ) + def set_get_administrator_function(self, + new_function: Callable[[object], + list]): + """Set a new get_administrators function. + + This function should take bot as argument and return an updated list + of its administrators. + Example: + ```python + def get_administrators(bot): + admins = bot.db['users'].find(privileges=2) + return list(admins) + ``` + """ + self._get_administrators = new_function + + @property + def administrators(self): + return self._get_administrators(self) + async def message_router(self, update, user_record): """Route Telegram `message` update to appropriate message handler.""" for key, value in update.items(): diff --git a/davtelepot/messages.py b/davtelepot/messages.py index 57471ab..79dac20 100644 --- a/davtelepot/messages.py +++ b/davtelepot/messages.py @@ -134,14 +134,6 @@ default_admin_messages = { 'it': "Vecchio commit: {old_record[last_commit]}\n" "Nuovo commit: {new_record[last_commit]}", }, - 'davtelepot_version': { - 'en': "davtelepot version: " - "{old_record[davtelepot_version]} —> " - "{new_record[davtelepot_version]}", - 'it': "Versione di davtelepot: " - "{old_record[davtelepot_version]} —> " - "{new_record[davtelepot_version]}", - }, }, 'query_button': { 'error': { @@ -256,6 +248,14 @@ default_admin_messages = { "sessione" } }, + 'updates_available': { + 'header': { + 'en': "🔔 Updates available! ⬇️\n\n" + "Click to /restart bot", + 'it': "🔔 Aggiornamenti disponibili! ⬇\n\n" + "Clicka qui per fare il /restart", + }, + }, 'version_command': { 'reply_keyboard_button': { 'en': "Version #️⃣", @@ -275,6 +275,81 @@ default_admin_messages = { }, } +default_authorization_messages = { + 'auth_command': { + 'description': { + 'en': "Edit user permissions. To select a user, reply to " + "a message of theirs or write their username", + 'it': "Cambia il grado di autorizzazione di un utente " + "(in risposta o scrivendone lo username)" + }, + 'unhandled_case': { + 'en': "Unhandled case :/", + 'it': "Caso non previsto :/" + }, + 'instructions': { + 'en': "Reply with this command to a user or write " + "/auth username to edit their permissions.", + 'it': "Usa questo comando in risposta a un utente " + "oppure scrivi /auth username per " + "cambiarne il grado di autorizzazione." + }, + 'unknown_user': { + 'en': "Unknown user.", + 'it': "Utente sconosciuto." + }, + 'choose_user': { + 'en': "{n} users match your query. Please select one.", + 'it': "Ho trovato {n} utenti che soddisfano questi criteri.\n" + "Per procedere selezionane uno." + }, + 'no_match': { + 'en': "No user matches your query. Please try again.", + 'it': "Non ho trovato utenti che soddisfino questi criteri.\n" + "Prova di nuovo." + } + }, + 'ban_command': { + 'description': { + 'en': "Reply to a user with /ban to ban them", + 'it': "Banna l'utente (da usare in risposta)" + } + }, + 'auth_button': { + 'description': { + 'en': "Edit user permissions", + 'it': "Cambia il grado di autorizzazione di un utente" + }, + 'confirm': { + 'en': "Are you sure?", + 'it': "Sicuro sicuro?" + }, + 'back_to_user': { + 'en': "Back to user", + 'it': "Torna all'utente" + }, + 'permission_denied': { + 'user': { + 'en': "You cannot appoint this user!", + 'it': "Non hai l'autorità di modificare i permessi di questo " + "utente!" + }, + 'role': { + 'en': "You're not allowed to appoint someone to this role!", + 'it': "Non hai l'autorità di conferire questo permesso!" + } + }, + 'no_change': { + 'en': "No change suggested!", + 'it': "È già così!" + }, + 'appointed': { + 'en': "Permission granted", + 'it': "Permesso conferito" + } + }, +} + default_authorization_denied_message = { 'en': "You are not allowed to use this command, sorry.", 'it': "Non disponi di autorizzazioni sufficienti per questa richiesta, spiacente.", diff --git a/davtelepot/suggestions.py b/davtelepot/suggestions.py index 2b9b4f2..de30225 100644 --- a/davtelepot/suggestions.py +++ b/davtelepot/suggestions.py @@ -149,15 +149,6 @@ async def _suggestions_button(bot: davtelepot.bot.Bot, update, user_record, data when = datetime.datetime.now() with bot.db as db: registered_user = db['users'].find_one(telegram_id=user_id) - admins = [ - x['telegram_id'] - for x in db['users'].find( - privileges=[ - bot.Role.get_role_by_name('admin').code, - bot.Role.get_role_by_name('founder').code - ] - ) - ] db['suggestions'].update( dict( id=suggestion_id, @@ -176,11 +167,11 @@ async def _suggestions_button(bot: davtelepot.bot.Bot, update, user_record, data bot=bot, update=update, user_record=user_record, ) - for admin in admins: + for admin in bot.administrators: when += datetime.timedelta(seconds=1) asyncio.ensure_future( bot.send_message( - chat_id=admin, + chat_id=admin['telegram_id'], text=suggestion_message, parse_mode='HTML' ) @@ -248,8 +239,10 @@ async def _see_suggestions(bot: davtelepot.bot.Bot, update, user_record): ) -def init(telegram_bot: davtelepot.bot.Bot, suggestion_messages=default_suggestion_messages): +def init(telegram_bot: davtelepot.bot.Bot, suggestion_messages=None): """Set suggestion handling for `bot`.""" + if suggestion_messages is None: + suggestion_messages = default_suggestion_messages telegram_bot.messages['suggestions'] = suggestion_messages suggestion_prefixes = ( list(suggestion_messages['suggestions_command']['reply_keyboard_button'].values()) diff --git a/setup.py b/setup.py index 240f4c8..bb987ca 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ setuptools.setup( 'bs4', 'dataset', ], + python_requires='>=3.5', classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console",