diff --git a/davtelepot/__init__.py b/davtelepot/__init__.py index e3cb9b6..051eb20 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.2.9" +__version__ = "2.3.0" __maintainer__ = "Davide Testa" __contact__ = "t.me/davte" diff --git a/davtelepot/bot.py b/davtelepot/bot.py index 70a3997..8b5df27 100644 --- a/davtelepot/bot.py +++ b/davtelepot/bot.py @@ -181,6 +181,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): self.messages['reply_keyboard_buttons'] = dict() self._unknown_command_message = None self.text_message_parsers = OrderedDict() + # Support for /help command + self.messages['help_sections'] = OrderedDict() # Handle location messages self.individual_location_handlers = dict() # Callback query-related properties @@ -1577,6 +1579,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): def command(self, command, aliases=None, reply_keyboard_button=None, show_in_keyboard=False, description="", + help_section=None, authorization_level='admin'): """Associate a bot command with a custom handler function. @@ -1598,6 +1601,22 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): default keyboard. `description` can be used to help users understand what `/command` does. + `help_section` is a dict on which the corresponding help section is + built. It may provide multilanguage support and should be + structured as follows: + { + "label": { # It will be displayed as button label + 'en': "Label", + ... + }, + "name": "section_name", + # If missing, `authorization_level` is used + "authorization_level": "everybody", + "description": { + 'en': "Description in English", + ... + }, + } `authorization_level` is the lowest authorization level needed to run the command. """ @@ -1619,6 +1638,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject): raise TypeError( f'Aliases {aliases} is not a list of strings string' ) + if isinstance(help_section, dict): + if 'authorization_level' not in help_section: + help_section['authorization_level'] = authorization_level + self.messages['help_sections'][help_section['name']] = help_section command = command.strip('/ ').lower() def command_decorator(command_handler): diff --git a/davtelepot/custombot.py b/davtelepot/custombot.py index 9b59290..2cb3749 100644 --- a/davtelepot/custombot.py +++ b/davtelepot/custombot.py @@ -373,6 +373,7 @@ class Bot(davtelepot.bot.Bot): def command(self, command, aliases=None, show_in_keyboard=False, reply_keyboard_button=None, descr="", auth='admin', description=None, + help_section=None, authorization_level=None): """Define a bot command. @@ -387,6 +388,7 @@ class Bot(davtelepot.bot.Bot): reply_keyboard_button=reply_keyboard_button, show_in_keyboard=show_in_keyboard, description=description, + help_section=help_section, authorization_level=authorization_level ) diff --git a/davtelepot/helper.py b/davtelepot/helper.py new file mode 100644 index 0000000..c79dce7 --- /dev/null +++ b/davtelepot/helper.py @@ -0,0 +1,226 @@ +"""Make a self-consistent bot help section.""" + +# Third party modules +from davtelepot.utilities import ( + get_cleaned_text, make_inline_keyboard, + make_lines_of_buttons, make_button +) + +# Project modules +from .messages import default_help_messages + + +def get_command_description(bot, update, user_record): + """Get a string description of `bot` commands. + + Show only commands available for `update` sender. + """ + user_role = bot.Role.get_user_role( + user_record=user_record + ) + return "\n".join( + [ + "/{}: {}".format( + command, + bot.get_message( + 'commands', command, 'description', + user_record=user_record, update=update, + default_message=( + details['description'] + if type(details['description']) is str + else '' + ) + ) + ) + for command, details in sorted( + bot.commands.items(), + key=lambda x:x[0] + ) + if details['description'] + and user_role.code <= bot.Role.get_user_role( + user_role_id=details['authorization_level'] + ).code + ] + ) + + +def _make_button(text=None, callback_data='', + prefix='help:///', delimiter='|', data=[]): + return make_button(text=text, callback_data=callback_data, + prefix=prefix, delimiter=delimiter, data=data) + + +def get_back_to_help_menu_keyboard(bot, update, user_record): + """Return a keyboard to let user come back to help menu.""" + return make_inline_keyboard( + [ + _make_button( + text=bot.get_message( + 'help', 'help_command', 'back_to_help_menu', + update=update, user_record=user_record + ), + data=['menu'] + ) + ], + 1 + ) + + +def get_help_buttons(bot, update, user_record): + """Get `bot` help menu inline keyboard. + + Show only buttons available for `update` sender. + """ + user_role = bot.Role.get_user_role( + user_record=user_record + ) + buttons_list = [ + _make_button( + text=bot.get_message( + 'help_sections', section['name'], 'label', + update=update, user_record=user_record, + ), + data=['section', name] + ) + for name, section in bot.messages['help_sections'].items() + if 'authorization_level' in section + and user_role.code <= bot.Role.get_user_role( + user_role_id=section['authorization_level'] + ).code + ] + return dict( + inline_keyboard=( + make_lines_of_buttons(buttons_list, 3) + + make_lines_of_buttons( + [ + _make_button( + text=bot.get_message( + 'help', 'commands_button_label', + update=update, user_record=user_record, + ), + data=['commands'] + ) + ], + 1 + ) + ) + ) + + +async def _help_command(bot, update, user_record): + if not bot.authorization_function(update=update, + authorization_level='everybody'): + return bot.get_message( + 'help', 'help_command', 'access_denied_message', + update=update, user_record=user_record + ) + reply_markup = get_help_buttons(bot, update, user_record) + return dict( + text=bot.get_message( + 'help', 'help_command', 'text', + update=update, user_record=user_record, + bot=bot + ), + parse_mode='HTML', + reply_markup=reply_markup, + disable_web_page_preview=True + ) + + +async def _help_button(bot, update, user_record, data): + result, text, reply_markup = '', '', None + if data[0] == 'commands': + text = bot.get_message( + 'help', 'help_command', 'header', + update=update, user_record=user_record, + bot=bot, + commands=get_command_description(bot, update, user_record) + ) + reply_markup = get_back_to_help_menu_keyboard( + bot=bot, update=update, user_record=user_record + ) + elif data[0] == 'menu': + text = bot.get_message( + 'help', 'help_command', 'text', + update=update, user_record=user_record, + bot=bot + ) + reply_markup = get_help_buttons(bot, update, user_record) + elif ( + data[0] == 'section' + and len(data) > 1 + and data[1] in bot.messages['help_sections'] + ): + section = bot.messages['help_sections'][data[1]] + if bot.authorization_function( + update=update, + authorization_level=section['authorization_level'] + ): + text = ( + "{label}\n\n" + "{description}" + ).format( + label=bot.get_message( + 'help_sections', section['name'], 'label', + update=update, user_record=user_record, + ), + description=bot.get_message( + 'help_sections', section['name'], 'description', + update=update, user_record=user_record, + bot=bot + ), + ) + else: + text = bot.authorization_denied_message + reply_markup = get_back_to_help_menu_keyboard( + bot=bot, update=update, user_record=user_record + ) + if text or reply_markup: + return dict( + text=result, + edit=dict( + text=text, + parse_mode='HTML', + reply_markup=reply_markup, + disable_web_page_preview=True + ) + ) + return result + + +async def _start_command(bot, update, user_record): + text = get_cleaned_text(update=update, bot=bot, replace=['start']) + if not text: + return await _help_command(bot, update, user_record) + update['text'] = text + await bot.text_message_handler( + update=update, + user_record=None + ) + return + + +def init(bot, help_messages=None): + """Assign parsers, commands, buttons and queries to given `bot`.""" + if help_messages is None: + help_messages = default_help_messages + bot.messages['help'] = help_messages + + @bot.command("/start", authorization_level='everybody') + async def start_command(bot, update, user_record): + return await _start_command(bot, update, user_record) + + @bot.command(command='/help', aliases=['00help'], + reply_keyboard_button=help_messages['help_command'][ + 'reply_keyboard_button'], + show_in_keyboard=True, + description=help_messages['help_command']['description'], + authorization_level='everybody') + async def help_command(bot, update, user_record): + result = await _help_command(bot, update, user_record) + return result + + @bot.button(prefix='help:///', separator='|', + authorization_level='everybody') + async def help_button(bot, update, user_record, data): + return await _help_button(bot, update, user_record, data) diff --git a/davtelepot/messages.py b/davtelepot/messages.py new file mode 100644 index 0000000..c180f89 --- /dev/null +++ b/davtelepot/messages.py @@ -0,0 +1,39 @@ +"""Default messages for bot functions.""" + +default_help_messages = { + 'help_command': { + 'header': { + 'en': "{bot.name} commands\n\n" + "{commands}", + 'it': "Comandi di {bot.name}\n\n" + "{commands}", + }, + 'text': { + 'en': "Guide", + 'it': "Guida" + }, + 'reply_keyboard_button': { + 'en': "Help πŸ“–", + 'it': "Guida πŸ“–" + }, + 'description': { + 'en': "Help", + 'it': "Aiuto" + }, + 'access_denied_message': { + 'en': "Ask for authorization. If your request is accepted, send " + "/help command again to read the guide.", + 'it': "Chiedi di essere autorizzato: se la tua richiesta " + "verrΓ  accolta, ripeti il comando /help per leggere " + "il messaggio di aiuto." + }, + 'back_to_help_menu': { + 'en': "Back to guide menu πŸ“–", + 'it': "Torna al menu Guida πŸ“–", + }, + }, + 'commands_button_label': { + 'en': "Commands πŸ€–", + 'it': "Comandi πŸ€–", + }, +}