Merge pull request #4092 from MrKrautee/telegram
Telegram: specify custom keyboard in config
This commit is contained in:
		| @@ -87,6 +87,41 @@ Example configuration showing the different settings: | |||||||
|    }, |    }, | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## Create a custom keyboard (command shortcut buttons) | ||||||
|  |  | ||||||
|  | Telegram allows us to create a custom keyboard with buttons for commands. | ||||||
|  | The default custom keyboard looks like this. | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | [ | ||||||
|  |     ["/daily", "/profit", "/balance"], # row 1, 3 commands | ||||||
|  |     ["/status", "/status table", "/performance"], # row 2, 3 commands | ||||||
|  |     ["/count", "/start", "/stop", "/help"] # row 3, 4 commands | ||||||
|  | ] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Usage | ||||||
|  |  | ||||||
|  | You can create your own keyboard in `config.json`: | ||||||
|  |  | ||||||
|  | ``` json | ||||||
|  | "telegram": { | ||||||
|  |       "enabled": true, | ||||||
|  |       "token": "your_telegram_token", | ||||||
|  |       "chat_id": "your_telegram_chat_id", | ||||||
|  |       "keyboard": [    | ||||||
|  |           ["/daily", "/stats", "/balance", "/profit"], | ||||||
|  |           ["/status table", "/performance"], | ||||||
|  |           ["/reload_config", "/count", "/logs"] | ||||||
|  |       ] | ||||||
|  |    }, | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | !!! Note "Supported Commands" | ||||||
|  |     Only the following commands are allowed. Command arguments are not supported! | ||||||
|  |  | ||||||
|  |     `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` | ||||||
|  |  | ||||||
| ## Telegram commands | ## Telegram commands | ||||||
|  |  | ||||||
| Per default, the Telegram bot shows predefined commands. Some commands | Per default, the Telegram bot shows predefined commands. Some commands | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ This module manage Telegram communication | |||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  | from itertools import chain | ||||||
| from typing import Any, Callable, Dict, List, Union | from typing import Any, Callable, Dict, List, Union | ||||||
|  |  | ||||||
| import arrow | import arrow | ||||||
| @@ -16,6 +17,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater | |||||||
| from telegram.utils.helpers import escape_markdown | from telegram.utils.helpers import escape_markdown | ||||||
|  |  | ||||||
| from freqtrade.__init__ import __version__ | from freqtrade.__init__ import __version__ | ||||||
|  | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.rpc import RPC, RPCException, RPCMessageType | from freqtrade.rpc import RPC, RPCException, RPCMessageType | ||||||
| from freqtrade.rpc.fiat_convert import CryptoToFiatConverter | from freqtrade.rpc.fiat_convert import CryptoToFiatConverter | ||||||
|  |  | ||||||
| @@ -74,10 +76,49 @@ class Telegram(RPC): | |||||||
|  |  | ||||||
|         self._updater: Updater |         self._updater: Updater | ||||||
|         self._config = freqtrade.config |         self._config = freqtrade.config | ||||||
|  |         self._init_keyboard() | ||||||
|         self._init() |         self._init() | ||||||
|         if self._config.get('fiat_display_currency', None): |         if self._config.get('fiat_display_currency', None): | ||||||
|             self._fiat_converter = CryptoToFiatConverter() |             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: |     def _init(self) -> None: | ||||||
|         """ |         """ | ||||||
|         Initializes this module with the given config, |         Initializes this module with the given config, | ||||||
| @@ -861,15 +902,7 @@ class Telegram(RPC): | |||||||
|         :param parse_mode: telegram parse mode |         :param parse_mode: telegram parse mode | ||||||
|         :return: None |         :return: None | ||||||
|         """ |         """ | ||||||
|  |         reply_markup = ReplyKeyboardMarkup(self._keyboard) | ||||||
|         keyboard: List[List[Union[str, KeyboardButton]]] = [ |  | ||||||
|             ['/daily', '/profit', '/balance'], |  | ||||||
|             ['/status', '/status table', '/performance'], |  | ||||||
|             ['/count', '/start', '/stop', '/help'] |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         reply_markup = ReplyKeyboardMarkup(keyboard) |  | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             try: |             try: | ||||||
|                 self._updater.bot.send_message( |                 self._updater.bot.send_message( | ||||||
|   | |||||||
| @@ -10,12 +10,13 @@ from unittest.mock import ANY, MagicMock, PropertyMock | |||||||
|  |  | ||||||
| import arrow | import arrow | ||||||
| import pytest | import pytest | ||||||
| from telegram import Chat, Message, Update | from telegram import Chat, Message, ReplyKeyboardMarkup, Update | ||||||
| from telegram.error import NetworkError | from telegram.error import NetworkError | ||||||
|  |  | ||||||
| from freqtrade import __version__ | from freqtrade import __version__ | ||||||
| from freqtrade.constants import CANCEL_REASON | from freqtrade.constants import CANCEL_REASON | ||||||
| from freqtrade.edge import PairInfo | from freqtrade.edge import PairInfo | ||||||
|  | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.freqtradebot import FreqtradeBot | from freqtrade.freqtradebot import FreqtradeBot | ||||||
| from freqtrade.loggers import setup_logging | from freqtrade.loggers import setup_logging | ||||||
| from freqtrade.persistence import PairLocks, Trade | 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 |     # Bot should've tried to send it twice | ||||||
|     assert len(bot.method_calls) == 2 |     assert len(bot.method_calls) == 2 | ||||||
|     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) |     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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user