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 | ||||
|  | ||||
| Per default, the Telegram bot shows predefined commands. Some commands | ||||
|   | ||||
| @@ -6,6 +6,7 @@ This module manage Telegram communication | ||||
| 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.ext import CallbackContext, CommandHandler, Updater | ||||
| 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 @@ class Telegram(RPC): | ||||
|  | ||||
|         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 @@ class Telegram(RPC): | ||||
|         :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( | ||||
|   | ||||
| @@ -10,12 +10,13 @@ from unittest.mock import ANY, MagicMock, PropertyMock | ||||
|  | ||||
| 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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user