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 π€",
+ },
+}