Package updates checker implemented

Notify administrators when new versions are available for PyPi packages in `bot.packages`.
This commit is contained in:
Davte 2020-04-27 13:48:33 +02:00
parent 5a70dcfeb2
commit edb2201773
8 changed files with 247 additions and 150 deletions

View File

@ -14,7 +14,7 @@ __author__ = "Davide Testa"
__email__ = "davide@davte.it" __email__ = "davide@davte.it"
__credits__ = ["Marco Origlia", "Nick Lee @Nickoala"] __credits__ = ["Marco Origlia", "Nick Lee @Nickoala"]
__license__ = "GNU General Public License v3.0" __license__ = "GNU General Public License v3.0"
__version__ = "2.4.25" __version__ = "2.5.0"
__maintainer__ = "Davide Testa" __maintainer__ = "Davide Testa"
__contact__ = "t.me/davte" __contact__ = "t.me/davte"

View File

@ -12,17 +12,19 @@ davtelepot.admin_tools.init(my_bot)
import asyncio import asyncio
import datetime import datetime
import json import json
import logging
# Third party modules # Third party modules
import davtelepot from sqlalchemy.exc import ResourceClosedError
from davtelepot import messages
from davtelepot.utilities import ( # Project modules
async_wrapper, Confirmator, extract, get_cleaned_text, get_user, from . import bot as davtelepot_bot, messages, __version__
escape_html_chars, line_drawing_unordered_list, make_button, 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, make_inline_keyboard, remove_html_tags, send_part_of_text_file,
send_csv_file send_csv_file
) )
from sqlalchemy.exc import ResourceClosedError
async def _forward_to(update, bot, sender, addressee, is_admin=False): 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 key: val
for key, val in user.items() for key, val in user.items()
if key in ( if key in ('first_name',
'first_name',
'last_name', 'last_name',
'username' 'username')
)
} }
) )
), ),
@ -779,7 +779,7 @@ def get_maintenance_exception_criterion(bot, allowed_command):
return criterion return criterion
async def get_version(): async def get_last_commit():
"""Get last commit hash and davtelepot version.""" """Get last commit hash and davtelepot version."""
try: try:
_subprocess = await asyncio.create_subprocess_exec( _subprocess = await asyncio.create_subprocess_exec(
@ -793,70 +793,132 @@ async def get_version():
last_commit = f"{e}" last_commit = f"{e}"
if last_commit.startswith("fatal: not a git repository"): if last_commit.startswith("fatal: not a git repository"):
last_commit = "-" last_commit = "-"
davtelepot_version = davtelepot.__version__ return last_commit
return last_commit, davtelepot_version
async def _version_command(bot, update, user_record): 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( return bot.get_message(
'admin', 'version_command', 'result', 'admin', 'version_command', 'result',
last_commit=last_commit, last_commit=last_commit,
davtelepot_version=davtelepot_version, davtelepot_version=__version__,
update=update, user_record=user_record 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 `bot` administrators about new versions.
Notify admins when last commit and/or davtelepot version change. 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( old_record = bot.db['version_history'].find_one(
order_by=['-id'] 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: if old_record is None:
old_record = dict( old_record = dict(
updated_at=datetime.datetime.min, updated_at=datetime.datetime.min,
last_commit=None,
davtelepot_version=None
) )
if ( for name in current_versions.keys():
old_record['last_commit'] != last_commit if name not in old_record:
or old_record['davtelepot_version'] != davtelepot_version 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( bot.db['version_history'].insert(
new_record dict(
updated_at=datetime.datetime.now(),
**current_versions
)
)
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"<b>{name[:-len('_version')]}</b>: "
f"<code>{old_record[name]}</code> —> "
f"<code>{current_version}</code>"
for name, current_version in current_versions.items()
if name not in ('last_commit', )
and current_version != old_record[name]
) )
for admin in bot.db['users'].find(privileges=[1, 2]):
await bot.send_message( await bot.send_message(
chat_id=admin['telegram_id'], chat_id=admin['telegram_id'],
disable_notification=True, disable_notification=True,
text='\n\n'.join( text=text
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')
)
)
) )
return 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"<b>{package}</b>: "
f"<code>{versions['current']}</code> —> "
f"<code>{versions['new']}</code>"
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`.""" """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: if talk_messages is None:
talk_messages = messages.default_talk_messages talk_messages = messages.default_talk_messages
telegram_bot.messages['talk'] = talk_messages telegram_bot.messages['talk'] = talk_messages
@ -1045,7 +1107,7 @@ def init(telegram_bot, talk_messages=None, admin_messages=None):
'help_section',) 'help_section',)
}, },
show_in_keyboard=False, show_in_keyboard=False,
authorization_level='admin',) authorization_level='admin')
async def version_command(bot, update, user_record): async def version_command(bot, update, user_record):
return await _version_command(bot=bot, return await _version_command(bot=bot,
update=update, update=update,

View File

@ -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. All methods and parameters are the same as the original json API.
A simple aiohttp asynchronous web client is used to make requests. A simple aiohttp asynchronous web client is used to make requests.
@ -10,11 +10,11 @@ import datetime
import json import json
import logging import logging
# Third party modules
from typing import Union, List from typing import Union, List
# Third party modules
import aiohttp import aiohttp
from aiohttp import web import aiohttp.web
class TelegramError(Exception): class TelegramError(Exception):
@ -82,7 +82,7 @@ class TelegramBot:
""" """
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
app = web.Application() app = aiohttp.web.Application()
sessions_timeouts = { sessions_timeouts = {
'getUpdates': dict( 'getUpdates': dict(
timeout=35, timeout=35,

View File

@ -2,9 +2,11 @@
# Standard library modules # Standard library modules
from collections import OrderedDict from collections import OrderedDict
from typing import Callable, Union
# Project modules # Project modules
from .bot import Bot from .bot import Bot
from .messages import default_authorization_messages
from .utilities import ( from .utilities import (
Confirmator, get_cleaned_text, get_user, make_button, make_inline_keyboard Confirmator, get_cleaned_text, get_user, make_button, make_inline_keyboard
) )
@ -256,85 +258,10 @@ def get_authorization_function(bot):
return is_authorized 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': "<code>Unhandled case :/</code>",
'it': "<code>Caso non previsto :/</code>"
},
'instructions': {
'en': "Reply with this command to a user or write "
"<code>/auth username</code> to edit their permissions.",
'it': "Usa questo comando in risposta a un utente "
"oppure scrivi <code>/auth username</code> 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): async def _authorization_command(bot, update, user_record):
text = get_cleaned_text(bot=bot, update=update, replace=['auth']) text = get_cleaned_text(bot=bot, update=update, replace=['auth'])
reply_markup = None reply_markup = None
# noinspection PyUnusedLocal
result = bot.get_message( result = bot.get_message(
'authorization', 'auth_command', 'unhandled_case', 'authorization', 'auth_command', 'unhandled_case',
update=update, user_record=user_record update=update, user_record=user_record
@ -509,7 +436,17 @@ async def _ban_command(bot, update, user_record):
return 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. """Set bot roles and assign role-related commands.
Pass an OrderedDict of `roles` to get them set. 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( telegram_bot.set_authorization_function(
get_authorization_function(telegram_bot) get_authorization_function(telegram_bot)
) )
if authorization_messages is None: get_administrators_function = (get_administrators_function
authorization_messages = deafult_authorization_messages 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.messages['authorization'] = authorization_messages
@telegram_bot.command(command='/auth', aliases=[], show_in_keyboard=False, @telegram_bot.command(command='/auth', aliases=[], show_in_keyboard=False,

View File

@ -34,13 +34,16 @@ Usage
# Standard library modules # Standard library modules
import asyncio import asyncio
from collections import OrderedDict
import datetime import datetime
import io import io
import inspect import inspect
import logging import logging
import os import os
import re import re
import sys
from collections import OrderedDict
from typing import Callable
# Third party modules # Third party modules
from aiohttp import web from aiohttp import web
@ -210,6 +213,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
if 'chat' in update if 'chat' in update
else None 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 # Message to be returned if user is not allowed to call method
self._authorization_denied_message = None self._authorization_denied_message = None
# Default authorization function (always return True) # Default authorization function (always return True)
@ -223,6 +228,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
self.placeholder_requests = dict() self.placeholder_requests = dict()
self.shared_data = dict() self.shared_data = dict()
self.Role = None self.Role = None
self.packages = [sys.modules['davtelepot']]
# Add `users` table with its fields if missing # Add `users` table with its fields if missing
if 'users' not in self.db.tables: if 'users' not in self.db.tables:
table = self.db.create_table( table = self.db.create_table(
@ -553,6 +559,26 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
default_inline_query_answer 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): async def message_router(self, update, user_record):
"""Route Telegram `message` update to appropriate message handler.""" """Route Telegram `message` update to appropriate message handler."""
for key, value in update.items(): for key, value in update.items():

View File

@ -134,14 +134,6 @@ default_admin_messages = {
'it': "Vecchio commit: <code>{old_record[last_commit]}</code>\n" 'it': "Vecchio commit: <code>{old_record[last_commit]}</code>\n"
"Nuovo commit: <code>{new_record[last_commit]}</code>", "Nuovo commit: <code>{new_record[last_commit]}</code>",
}, },
'davtelepot_version': {
'en': "davtelepot version: "
"<code>{old_record[davtelepot_version]}</code> —> "
"<code>{new_record[davtelepot_version]}</code>",
'it': "Versione di davtelepot: "
"<code>{old_record[davtelepot_version]}</code> —> "
"<code>{new_record[davtelepot_version]}</code>",
},
}, },
'query_button': { 'query_button': {
'error': { 'error': {
@ -256,6 +248,14 @@ default_admin_messages = {
"sessione" "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': { 'version_command': {
'reply_keyboard_button': { 'reply_keyboard_button': {
'en': "Version #️⃣", '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': "<code>Unhandled case :/</code>",
'it': "<code>Caso non previsto :/</code>"
},
'instructions': {
'en': "Reply with this command to a user or write "
"<code>/auth username</code> to edit their permissions.",
'it': "Usa questo comando in risposta a un utente "
"oppure scrivi <code>/auth username</code> 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 = { default_authorization_denied_message = {
'en': "You are not allowed to use this command, sorry.", 'en': "You are not allowed to use this command, sorry.",
'it': "Non disponi di autorizzazioni sufficienti per questa richiesta, spiacente.", 'it': "Non disponi di autorizzazioni sufficienti per questa richiesta, spiacente.",

View File

@ -149,15 +149,6 @@ async def _suggestions_button(bot: davtelepot.bot.Bot, update, user_record, data
when = datetime.datetime.now() when = datetime.datetime.now()
with bot.db as db: with bot.db as db:
registered_user = db['users'].find_one(telegram_id=user_id) 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( db['suggestions'].update(
dict( dict(
id=suggestion_id, id=suggestion_id,
@ -176,11 +167,11 @@ async def _suggestions_button(bot: davtelepot.bot.Bot, update, user_record, data
bot=bot, bot=bot,
update=update, user_record=user_record, update=update, user_record=user_record,
) )
for admin in admins: for admin in bot.administrators:
when += datetime.timedelta(seconds=1) when += datetime.timedelta(seconds=1)
asyncio.ensure_future( asyncio.ensure_future(
bot.send_message( bot.send_message(
chat_id=admin, chat_id=admin['telegram_id'],
text=suggestion_message, text=suggestion_message,
parse_mode='HTML' 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`.""" """Set suggestion handling for `bot`."""
if suggestion_messages is None:
suggestion_messages = default_suggestion_messages
telegram_bot.messages['suggestions'] = suggestion_messages telegram_bot.messages['suggestions'] = suggestion_messages
suggestion_prefixes = ( suggestion_prefixes = (
list(suggestion_messages['suggestions_command']['reply_keyboard_button'].values()) list(suggestion_messages['suggestions_command']['reply_keyboard_button'].values())

View File

@ -59,6 +59,7 @@ setuptools.setup(
'bs4', 'bs4',
'dataset', 'dataset',
], ],
python_requires='>=3.5',
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",