Repository: freqtrade/freqtrade

Issue:
modify the telegram buttons that send commands to the bot
Debian 10
Python 3.7.3
ccxt==1.38.55
freqtrade develop-058d40a7

I am not sure if this is the right place to ask this, if it's not, let me know.

Is there a way to add/remove/modify the telegram buttons that send commands to the bot?

![buttons](https://user-images.githubusercontent.com/528302/102501677-5de4c080-4086-11eb-8392-9436be175592.png)

Thanks


Discussion:
There is currently no way to modify this.
#3942 proposes to allow the user to customize this (which makes this a duplicate)- but i don't think anyone is working on that at the moment, so if you think you need this, feel free to pick it up - it'd be a great place to get started with contributing to freqtrade, and therefore giving something back to the community.

While changing the default we have now may or may not make sense - it must be optimized for portrait mode of a phone - which doesn't allow more buttons next to each other (it's already pretty cramped as is).


Is it ok if I modify freqtrade/freqtrade/rpc/telegram.py ?
Or will I cause other issues?

I think it's this part

![keys](https://user-images.githubusercontent.com/528302/102503934-e8c6ba80-4088-11eb-95d3-a4b4c403fb51.png)

it's open source, you can modify whatever you want.

Should there be upstream changes to this (in this repository), then you might encounter problems with upgrades which you then need to figure out yourself.

Resolving patch:
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -6,6 +6,7 @@
 import json
 import logging
 from datetime import timedelta
+from itertools import chain
 from typing import Any, Callable, Dict, List, Union
 
 import arrow
@@ -16,6 +17,7 @@
 from telegram.utils.helpers import escape_markdown
 
 from freqtrade.__init__ import __version__
+from freqtrade.exceptions import OperationalException
 from freqtrade.rpc import RPC, RPCException, RPCMessageType
 from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
 
@@ -74,10 +76,49 @@ def __init__(self, freqtrade) -> None:
 
         self._updater: Updater
         self._config = freqtrade.config
+        self._init_keyboard()
         self._init()
         if self._config.get('fiat_display_currency', None):
             self._fiat_converter = CryptoToFiatConverter()
 
+    def _init_keyboard(self) -> None:
+        """
+        Validates the keyboard configuration from telegram config
+        section.
+        """
+        self._keyboard: List[List[Union[str, KeyboardButton]]] = [
+            ['/daily', '/profit', '/balance'],
+            ['/status', '/status table', '/performance'],
+            ['/count', '/start', '/stop', '/help']
+        ]
+        # do not allow commands with mandatory arguments and critical cmds
+        # like /forcesell and /forcebuy
+        # TODO: DRY! - its not good to list all valid cmds here. But otherwise
+        #       this needs refacoring of the whole telegram module (same
+        #       problem in _help()).
+        valid_keys: List[str] = ['/start', '/stop', '/status', '/status table',
+                                 '/trades', '/profit', '/performance', '/daily',
+                                 '/stats', '/count', '/locks', '/balance',
+                                 '/stopbuy', '/reload_config', '/show_config',
+                                 '/logs', '/whitelist', '/blacklist', '/edge',
+                                 '/help', '/version']
+
+        # custom keyboard specified in config.json
+        cust_keyboard = self._config['telegram'].get('keyboard', [])
+        if cust_keyboard:
+            # check for valid shortcuts
+            invalid_keys = [b for b in chain.from_iterable(cust_keyboard)
+                            if b not in valid_keys]
+            if len(invalid_keys):
+                err_msg = ('config.telegram.keyboard: Invalid commands for '
+                           f'custom Telegram keyboard: {invalid_keys}'
+                           f'\nvalid commands are: {valid_keys}')
+                raise OperationalException(err_msg)
+            else:
+                self._keyboard = cust_keyboard
+                logger.info('using custom keyboard from '
+                            f'config.json: {self._keyboard}')
+
     def _init(self) -> None:
         """
         Initializes this module with the given config,
@@ -861,15 +902,7 @@ def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
         :param parse_mode: telegram parse mode
         :return: None
         """
-
-        keyboard: List[List[Union[str, KeyboardButton]]] = [
-            ['/daily', '/profit', '/balance'],
-            ['/status', '/status table', '/performance'],
-            ['/count', '/start', '/stop', '/help']
-        ]
-
-        reply_markup = ReplyKeyboardMarkup(keyboard)
-
+        reply_markup = ReplyKeyboardMarkup(self._keyboard)
         try:
             try:
                 self._updater.bot.send_message(


Test patch:
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -10,12 +10,13 @@
 
 import arrow
 import pytest
-from telegram import Chat, Message, Update
+from telegram import Chat, Message, ReplyKeyboardMarkup, Update
 from telegram.error import NetworkError
 
 from freqtrade import __version__
 from freqtrade.constants import CANCEL_REASON
 from freqtrade.edge import PairInfo
+from freqtrade.exceptions import OperationalException
 from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.loggers import setup_logging
 from freqtrade.persistence import PairLocks, Trade
@@ -1729,3 +1730,53 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     # Bot should've tried to send it twice
     assert len(bot.method_calls) == 2
     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
+
+
+def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
+    mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
+    bot = MagicMock()
+    bot.send_message = MagicMock()
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
+
+    invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']]
+    default_keys_list = [['/daily', '/profit', '/balance'],
+                         ['/status', '/status table', '/performance'],
+                         ['/count', '/start', '/stop', '/help']]
+    default_keyboard = ReplyKeyboardMarkup(default_keys_list)
+
+    custom_keys_list = [['/daily', '/stats', '/balance', '/profit'],
+                        ['/count', '/start', '/reload_config', '/help']]
+    custom_keyboard = ReplyKeyboardMarkup(custom_keys_list)
+
+    def init_telegram(freqtradebot):
+        telegram = Telegram(freqtradebot)
+        telegram._updater = MagicMock()
+        telegram._updater.bot = bot
+        return telegram
+
+    # no keyboard in config -> default keyboard
+    freqtradebot.config['telegram']['enabled'] = True
+    telegram = init_telegram(freqtradebot)
+    telegram._send_msg('test')
+    used_keyboard = bot.send_message.call_args[1]['reply_markup']
+    assert used_keyboard == default_keyboard
+
+    # invalid keyboard in config -> default keyboard
+    freqtradebot.config['telegram']['enabled'] = True
+    freqtradebot.config['telegram']['keyboard'] = invalid_keys_list
+    err_msg = re.escape("config.telegram.keyboard: Invalid commands for custom "
+                        "Telegram keyboard: ['/not_valid', '/alsoinvalid']"
+                        "\nvalid commands are: ") + r"*"
+    with pytest.raises(OperationalException, match=err_msg):
+        telegram = init_telegram(freqtradebot)
+
+    # valid keyboard in config -> custom keyboard
+    freqtradebot.config['telegram']['enabled'] = True
+    freqtradebot.config['telegram']['keyboard'] = custom_keys_list
+    telegram = init_telegram(freqtradebot)
+    telegram._send_msg('test')
+    used_keyboard = bot.send_message.call_args[1]['reply_markup']
+    assert used_keyboard == custom_keyboard
+    assert log_has("using custom keyboard from config.json: "
+                   "[['/daily', '/stats', '/balance', '/profit'], ['/count', "
+                   "'/start', '/reload_config', '/help']]", caplog)