Package update notifier completed

This commit is contained in:
Davte
2020-04-27 13:45:25 +02:00
parent 16014c5a42
commit 1e2316e748
6 changed files with 211 additions and 141 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

@@ -18,7 +18,7 @@ import logging
from sqlalchemy.exc import ResourceClosedError from sqlalchemy.exc import ResourceClosedError
# Project modules # Project modules
from . import bot as davtelepot_bot, messages, __version__ as version from . import bot as davtelepot_bot, messages, __version__
from .utilities import ( from .utilities import (
async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text, async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text,
get_user, escape_html_chars, line_drawing_unordered_list, make_button, get_user, escape_html_chars, line_drawing_unordered_list, make_button,
@@ -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,64 +793,74 @@ 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 = 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.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"<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]
)
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
@@ -858,6 +868,7 @@ async def notify_new_version(bot):
async def get_package_updates(bot: davtelepot_bot, async def get_package_updates(bot: davtelepot_bot,
monitoring_interval: int = 60 * 60): monitoring_interval: int = 60 * 60):
while 1: while 1:
news = dict()
for package in bot.packages: for package in bot.packages:
package_web_page = CachedPage.get( package_web_page = CachedPage.get(
f'https://pypi.python.org/pypi/{package.__name__}/json', f'https://pypi.python.org/pypi/{package.__name__}/json',
@@ -872,15 +883,42 @@ async def get_package_updates(bot: davtelepot_bot,
new_version = web_page['info']['version'] new_version = web_page['info']['version']
current_version = package.__version__ current_version = package.__version__
if new_version != current_version: if new_version != current_version:
print(f"New version of {package}: " news[package.__name__] = {
f"<code>{current_version}</code> > " 'current': current_version,
f"<code>{new_version}</code>") 'new': new_version
# TODO notify administrators }
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) await asyncio.sleep(monitoring_interval)
def init(telegram_bot, talk_messages=None, admin_messages=None): 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
@@ -902,8 +940,6 @@ def init(telegram_bot, talk_messages=None, admin_messages=None):
for command in ['stop', 'restart', 'maintenance'] for command in ['stop', 'restart', 'maintenance']
] ]
asyncio.ensure_future(get_package_updates(telegram_bot))
@telegram_bot.additional_task(when='BEFORE') @telegram_bot.additional_task(when='BEFORE')
async def load_talking_sessions(): async def load_talking_sessions():
sessions = [] sessions = []

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())