Merge pull request #8394 from freqtrade/dependabot/pip/develop/python-telegram-bot-20.2

Bump python-telegram-bot from 13.15 to 20.2
This commit is contained in:
Matthias 2023-04-24 15:58:39 +02:00 committed by GitHub
commit 459e5e67bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 501 additions and 406 deletions

View File

@ -107,8 +107,7 @@ class Exchange:
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
# Due to funding fee fetching.
self._loop_lock = Lock()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop = self._init_async_loop()
self._config: Config = {}
self._config.update(config)
@ -212,6 +211,11 @@ class Exchange:
if self.loop and not self.loop.is_closed():
self.loop.close()
def _init_async_loop(self) -> asyncio.AbstractEventLoop:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
def validate_config(self, config):
# Check if timeframe is available
self.validate_timeframes(config.get('timeframe'))

View File

@ -3,6 +3,7 @@
"""
This module manage Telegram communication
"""
import asyncio
import json
import logging
import re
@ -13,15 +14,17 @@ from functools import partial
from html import escape
from itertools import chain
from math import isnan
from typing import Any, Callable, Dict, List, Optional, Union
from threading import Thread
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
import arrow
from tabulate import tabulate
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update)
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
ReplyKeyboardMarkup, Update)
from telegram.constants import MessageLimit, ParseMode
from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler
from telegram.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN, Config
@ -33,6 +36,9 @@ from freqtrade.rpc import RPC, RPCException, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH
logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
@ -47,14 +53,14 @@ class TimeunitMappings:
default: int
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(self, *args, **kwargs):
async def wrapper(self, *args, **kwargs):
""" Decorator logic """
update = kwargs.get('update') or args[0]
@ -76,9 +82,9 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
chat_id
)
try:
return command_handler(self, *args, **kwargs)
return await command_handler(self, *args, **kwargs)
except RPCException as e:
self._send_msg(str(e))
await self._send_msg(str(e))
except BaseException:
logger.exception('Exception occurred within Telegram module')
finally:
@ -99,9 +105,17 @@ class Telegram(RPCHandler):
"""
super().__init__(rpc, config)
self._updater: Updater
self._app: Application
self._loop: asyncio.AbstractEventLoop
self._init_keyboard()
self._init()
self._start_thread()
def _start_thread(self):
"""
Creates and starts the polling thread
"""
self._thread = Thread(target=self._init, name='FTTelegram')
self._thread.start()
def _init_keyboard(self) -> None:
"""
@ -152,14 +166,23 @@ class Telegram(RPCHandler):
logger.info('using custom keyboard from '
f'config.json: {self._keyboard}')
def _init_telegram_app(self):
return Application.builder().token(self._config['telegram']['token']).build()
def _init(self) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
Runs in a separate thread.
"""
self._updater = Updater(token=self._config['telegram']['token'], workers=0,
use_context=True)
try:
self._loop = asyncio.get_running_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._app = self._init_telegram_app()
# Register command handler and start telegram message polling
handles = [
@ -218,21 +241,38 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
self._app.add_handler(handle)
for callback in callbacks:
self._updater.dispatcher.add_handler(callback)
self._app.add_handler(callback)
self._updater.start_polling(
bootstrap_retries=-1,
timeout=20,
read_latency=60, # Assumed transmission latency
drop_pending_updates=True,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
[[x for x in sorted(h.commands)] for h in handles]
)
self._loop.run_until_complete(self._startup_telegram())
async def _startup_telegram(self) -> None:
await self._app.initialize()
await self._app.start()
if self._app.updater:
await self._app.updater.start_polling(
bootstrap_retries=-1,
timeout=20,
# read_latency=60, # Assumed transmission latency
drop_pending_updates=True,
# stop_signals=[], # Necessary as we don't run on the main thread
)
while True:
await asyncio.sleep(10)
if not self._app.updater.running:
break
async def _cleanup_telegram(self) -> None:
if self._app.updater:
await self._app.updater.stop()
await self._app.stop()
await self._app.shutdown()
def cleanup(self) -> None:
"""
@ -240,7 +280,8 @@ class Telegram(RPCHandler):
:return: None
"""
# This can take up to `timeout` from the call to `start_polling`.
self._updater.stop()
asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop)
self._thread.join()
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
"""
@ -453,7 +494,9 @@ class Telegram(RPCHandler):
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
if message:
self._send_msg(message, disable_notification=(noti == 'silent'))
asyncio.run_coroutine_threadsafe(
self._send_msg(message, disable_notification=(noti == 'silent')),
self._loop)
def _get_sell_emoji(self, msg):
"""
@ -536,7 +579,7 @@ class Telegram(RPCHandler):
return lines_detail
@authorized_only
def _status(self, update: Update, context: CallbackContext) -> None:
async def _status(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /status.
Returns the current TradeThread status
@ -546,12 +589,12 @@ class Telegram(RPCHandler):
"""
if context.args and 'table' in context.args:
self._status_table(update, context)
await self._status_table(update, context)
return
else:
self._status_msg(update, context)
await self._status_msg(update, context)
def _status_msg(self, update: Update, context: CallbackContext) -> None:
async def _status_msg(self, update: Update, context: CallbackContext) -> None:
"""
handler for `/status` and `/status <id>`.
@ -635,9 +678,9 @@ class Telegram(RPCHandler):
lines_detail = self._prepare_order_details(
r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "")
self.__send_status_msg(lines, r)
await self.__send_status_msg(lines, r)
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
"""
Send status message.
"""
@ -648,13 +691,13 @@ class Telegram(RPCHandler):
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + '\n'
else:
self._send_msg(msg.format(**r))
await self._send_msg(msg.format(**r))
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
self._send_msg(msg.format(**r))
await self._send_msg(msg.format(**r))
@authorized_only
def _status_table(self, update: Update, context: CallbackContext) -> None:
async def _status_table(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
@ -687,12 +730,11 @@ class Telegram(RPCHandler):
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_status_table",
query=update.callback_query)
await self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_status_table",
query=update.callback_query)
@authorized_only
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
@ -739,11 +781,11 @@ class Telegram(RPCHandler):
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
f'<pre>{stats_tab}</pre>'
)
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path=val.callback, query=update.callback_query)
await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
callback_path=val.callback, query=update.callback_query)
@authorized_only
def _daily(self, update: Update, context: CallbackContext) -> None:
async def _daily(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
@ -751,10 +793,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'days')
await self._timeunit_stats(update, context, 'days')
@authorized_only
def _weekly(self, update: Update, context: CallbackContext) -> None:
async def _weekly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /weekly <n>
Returns a weekly profit (in BTC) over the last n weeks.
@ -762,10 +804,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'weeks')
await self._timeunit_stats(update, context, 'weeks')
@authorized_only
def _monthly(self, update: Update, context: CallbackContext) -> None:
async def _monthly(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /monthly <n>
Returns a monthly profit (in BTC) over the last n months.
@ -773,10 +815,10 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
self._timeunit_stats(update, context, 'months')
await self._timeunit_stats(update, context, 'months')
@authorized_only
def _profit(self, update: Update, context: CallbackContext) -> None:
async def _profit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
@ -850,11 +892,11 @@ class Telegram(RPCHandler):
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
)
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@authorized_only
def _stats(self, update: Update, context: CallbackContext) -> None:
async def _stats(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stats
Show stats of recent trades
@ -885,7 +927,7 @@ class Telegram(RPCHandler):
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
)
if len(exit_reasons_tabulate) > 25:
self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
exit_reasons_msg = ''
durations = stats['durations']
@ -900,10 +942,10 @@ class Telegram(RPCHandler):
)
msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""")
self._send_msg(msg, ParseMode.MARKDOWN)
await self._send_msg(msg, ParseMode.MARKDOWN)
@authorized_only
def _balance(self, update: Update, context: CallbackContext) -> None:
async def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """
full_result = context.args and 'full' in context.args
result = self._rpc._rpc_balance(self._config['stake_currency'],
@ -957,7 +999,7 @@ class Telegram(RPCHandler):
# Handle overflowing message length
if len(output + curr_output) >= MAX_MESSAGE_LENGTH:
self._send_msg(output)
await self._send_msg(output)
output = curr_output
else:
output += curr_output
@ -981,11 +1023,11 @@ class Telegram(RPCHandler):
f"\t`{result['stake']}: {total_stake}`{stake_improve}\n"
f"\t`{result['symbol']}: {value}`{fiat_val}\n"
)
self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
await self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
@authorized_only
def _start(self, update: Update, context: CallbackContext) -> None:
async def _start(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /start.
Starts TradeThread
@ -994,10 +1036,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_start()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stop(self, update: Update, context: CallbackContext) -> None:
async def _stop(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stop.
Stops TradeThread
@ -1006,10 +1048,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stop()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None:
async def _reload_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /reload_config.
Triggers a config file reload
@ -1018,10 +1060,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_reload_config()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stopentry(self, update: Update, context: CallbackContext) -> None:
async def _stopentry(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades
@ -1030,10 +1072,10 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stopentry()
self._send_msg(f"Status: `{msg['status']}`")
await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _force_exit(self, update: Update, context: CallbackContext) -> None:
async def _force_exit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /forceexit <id>.
Sells the given trade at current price
@ -1044,14 +1086,14 @@ class Telegram(RPCHandler):
if context.args:
trade_id = context.args[0]
self._force_exit_action(trade_id)
await self._force_exit_action(trade_id)
else:
fiat_currency = self._config.get('fiat_display_currency', '')
try:
statlist, _, _ = self._rpc._rpc_status_table(
self._config['stake_currency'], fiat_currency)
except RPCException:
self._send_msg(msg='No open trade found.')
await self._send_msg(msg='No open trade found.')
return
trades = []
for trade in statlist:
@ -1064,51 +1106,51 @@ class Telegram(RPCHandler):
buttons_aligned.append([InlineKeyboardButton(
text='Cancel', callback_data='force_exit__cancel')])
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
await self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
def _force_exit_action(self, trade_id):
async def _force_exit_action(self, trade_id):
if trade_id != 'cancel':
try:
self._rpc._rpc_force_exit(trade_id)
except RPCException as e:
self._send_msg(str(e))
await self._send_msg(str(e))
def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
if query.data and '__' in query.data:
# Input data is "force_exit__<tradid|cancel>"
trade_id = query.data.split("__")[1].split(' ')[0]
if trade_id == 'cancel':
query.answer()
query.edit_message_text(text="Force exit canceled.")
await query.answer()
await query.edit_message_text(text="Force exit canceled.")
return
trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
query.answer()
await query.answer()
if trade:
query.edit_message_text(
await query.edit_message_text(
text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
await self._force_exit_action(trade_id)
else:
query.edit_message_text(text=f"Trade {trade_id} not found.")
await query.edit_message_text(text=f"Trade {trade_id} not found.")
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
logger.exception("Forcebuy error!")
self._send_msg(str(e), ParseMode.HTML)
await self._send_msg(str(e), ParseMode.HTML)
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
if query.data and '_||_' in query.data:
pair, side = query.data.split('_||_')
order_side = SignalDirection(side)
query.answer()
query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
self._force_enter_action(pair, None, order_side)
await query.answer()
await query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
await self._force_enter_action(pair, None, order_side)
@staticmethod
def _layout_inline_keyboard(
@ -1121,7 +1163,7 @@ class Telegram(RPCHandler):
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only
def _force_enter(
async def _force_enter(
self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
"""
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
@ -1133,7 +1175,7 @@ class Telegram(RPCHandler):
if context.args:
pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None
self._force_enter_action(pair, price, order_side)
await self._force_enter_action(pair, price, order_side)
else:
whitelist = self._rpc._rpc_whitelist()['whitelist']
pair_buttons = [
@ -1143,12 +1185,12 @@ class Telegram(RPCHandler):
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
self._send_msg(msg="Which pair?",
keyboard=buttons_aligned,
query=update.callback_query)
await self._send_msg(msg="Which pair?",
keyboard=buttons_aligned,
query=update.callback_query)
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
async def _trades(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /trades <n>
Returns last n recent trades.
@ -1177,10 +1219,10 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
async def _delete_trade(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete <id>.
Delete the given trade
@ -1192,13 +1234,13 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
self._send_msg(
await self._send_msg(
f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.'
)
@authorized_only
def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
@ -1210,10 +1252,10 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
self._rpc._rpc_cancel_open_order(trade_id)
self._send_msg('Open order canceled.')
await self._send_msg('Open order canceled.')
@authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None:
async def _performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
@ -1231,17 +1273,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_performance",
query=update.callback_query)
@authorized_only
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /buys PAIR .
Shows a performance statistic from finished trades
@ -1263,17 +1305,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
@authorized_only
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /sells.
Shows a performance statistic from finished trades
@ -1295,17 +1337,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_exit_reason_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_exit_reason_performance",
query=update.callback_query)
@authorized_only
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /mix_tags.
Shows a performance statistic from finished trades
@ -1327,17 +1369,17 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
await self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_mix_tag_performance",
query=update.callback_query)
await self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_mix_tag_performance",
query=update.callback_query)
@authorized_only
def _count(self, update: Update, context: CallbackContext) -> None:
async def _count(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /count.
Returns the number of trades running
@ -1351,19 +1393,19 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = f"<pre>{message}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
query=update.callback_query)
await self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
query=update.callback_query)
@authorized_only
def _locks(self, update: Update, context: CallbackContext) -> None:
async def _locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /locks.
Returns the currently active locks
"""
rpc_locks = self._rpc._rpc_locks()
if not rpc_locks['locks']:
self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
await self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
for locks in chunks(rpc_locks['locks'], 25):
message = tabulate([[
@ -1375,10 +1417,10 @@ class Telegram(RPCHandler):
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
async def _delete_locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete_locks.
Returns the currently active locks
@ -1393,10 +1435,10 @@ class Telegram(RPCHandler):
pair = arg
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
self._locks(update, context)
await self._locks(update, context)
@authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None:
async def _whitelist(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /whitelist
Shows the currently active whitelist
@ -1413,39 +1455,39 @@ class Telegram(RPCHandler):
message += f"`{', '.join(whitelist['whitelist'])}`"
logger.debug(message)
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _blacklist(self, update: Update, context: CallbackContext) -> None:
async def _blacklist(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /blacklist
Shows the currently active blacklist
"""
self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
def send_blacklist_msg(self, blacklist: Dict):
async def send_blacklist_msg(self, blacklist: Dict):
errmsgs = []
for pair, error in blacklist['errors'].items():
errmsgs.append(f"Error: {error['error_msg']}")
if errmsgs:
self._send_msg('\n'.join(errmsgs))
await self._send_msg('\n'.join(errmsgs))
message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`"
logger.debug(message)
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /bl_delete
Deletes pair(s) from current blacklist
"""
self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
@authorized_only
def _logs(self, update: Update, context: CallbackContext) -> None:
async def _logs(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /logs
Shows the latest logs
@ -1464,17 +1506,17 @@ class Telegram(RPCHandler):
escape_markdown(logrec[4], version=2))
if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH:
# Send message immediately if it would become too long
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
msgs = msg + '\n'
else:
# Append message to messages to send
msgs += msg + '\n'
if msgs:
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
@authorized_only
def _edge(self, update: Update, context: CallbackContext) -> None:
async def _edge(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /edge
Shows information related to Edge
@ -1482,17 +1524,17 @@ class Telegram(RPCHandler):
edge_pairs = self._rpc._rpc_edge()
if not edge_pairs:
message = '<b>Edge only validated following pairs:</b>'
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
for chunk in chunks(edge_pairs, 25):
edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple')
message = (f'<b>Edge only validated following pairs:</b>\n'
f'<pre>{edge_pairs_tab}</pre>')
self._send_msg(message, parse_mode=ParseMode.HTML)
await self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(self, update: Update, context: CallbackContext) -> None:
async def _help(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /help.
Show commands of the bot
@ -1570,20 +1612,20 @@ class Telegram(RPCHandler):
"*/version:* `Show version`"
)
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
await self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
@authorized_only
def _health(self, update: Update, context: CallbackContext) -> None:
async def _health(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /health
Shows the last process timestamp
"""
health = self._rpc.health()
message = f"Last process: `{health['last_process_loc']}`"
self._send_msg(message)
await self._send_msg(message)
@authorized_only
def _version(self, update: Update, context: CallbackContext) -> None:
async def _version(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /version.
Show version information
@ -1596,10 +1638,10 @@ class Telegram(RPCHandler):
if strategy_version is not None:
version_string += f', *Strategy version: * `{strategy_version}`'
self._send_msg(version_string)
await self._send_msg(version_string)
@authorized_only
def _show_config(self, update: Update, context: CallbackContext) -> None:
async def _show_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /show_config.
Show config information information
@ -1628,7 +1670,7 @@ class Telegram(RPCHandler):
else:
pa_info = "*Position adjustment:* Off\n"
self._send_msg(
await self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Market: * `{val['trading_mode']}`\n"
@ -1644,8 +1686,8 @@ class Telegram(RPCHandler):
f"*Current state:* `{val['state']}`"
)
def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
if reload_able:
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)],
@ -1659,7 +1701,7 @@ class Telegram(RPCHandler):
message_id = query.message.message_id
try:
self._updater.bot.edit_message_text(
await self._app.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=msg,
@ -1674,12 +1716,12 @@ class Telegram(RPCHandler):
except TelegramError as telegram_err:
logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:
async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:
"""
Send given markdown message
:param msg: message
@ -1689,20 +1731,20 @@ class Telegram(RPCHandler):
"""
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
if query:
self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
callback_path=callback_path, reload_able=reload_able)
await self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
callback_path=callback_path, reload_able=reload_able)
return
if reload_able and self._config['telegram'].get('reload', True):
reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)]])
else:
if keyboard is not None:
reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
reply_markup = InlineKeyboardMarkup(keyboard)
else:
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
try:
try:
self._updater.bot.send_message(
await self._app.bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
@ -1716,7 +1758,7 @@ class Telegram(RPCHandler):
'Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
self._updater.bot.send_message(
await self._app.bot.send_message(
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
@ -1730,7 +1772,7 @@ class Telegram(RPCHandler):
)
@authorized_only
def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
async def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /marketdir.
Updates the bot's market_direction
@ -1753,14 +1795,14 @@ class Telegram(RPCHandler):
if new_market_dir is not None:
self._rpc._update_market_direction(new_market_dir)
self._send_msg("Successfully updated market direction"
f" from *{old_market_dir}* to *{new_market_dir}*.")
await self._send_msg("Successfully updated market direction"
f" from *{old_market_dir}* to *{new_market_dir}*.")
else:
raise RPCException("Invalid market direction provided. \n"
"Valid market directions: *long, short, even, none*")
elif context.args is not None and len(context.args) == 0:
old_market_dir = self._rpc._get_market_direction()
self._send_msg(f"Currently set market direction: *{old_market_dir}*")
await self._send_msg(f"Currently set market direction: *{old_market_dir}*")
else:
raise RPCException("Invalid usage of command /marketdir. \n"
"Usage: */marketdir [short | long | even | none]*")

View File

@ -18,8 +18,6 @@ pytest-random-order==1.1.0
isort==5.12.0
# For datetime mocking
time-machine==2.9.0
# fastapi testing
httpx==0.24.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.3.1

View File

@ -6,7 +6,9 @@ ccxt==3.0.75
cryptography==40.0.2
aiohttp==3.8.4
SQLAlchemy==2.0.10
python-telegram-bot==13.15
python-telegram-bot==20.2
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.23.3
arrow==1.2.3
cachetools==4.2.2
requests==2.28.2

View File

@ -3,7 +3,7 @@ import json
import logging
import re
from copy import deepcopy
from datetime import datetime, timedelta
from datetime import timedelta
from pathlib import Path
from typing import Optional
from unittest.mock import MagicMock, Mock, PropertyMock
@ -12,7 +12,6 @@ import arrow
import numpy as np
import pandas as pd
import pytest
from telegram import Chat, Message, Update
from freqtrade import constants
from freqtrade.commands import Arguments
@ -550,13 +549,6 @@ def get_default_conf_usdt(testdatadir):
return configuration
@pytest.fixture
def update():
_update = Update(0)
_update.message = Message(0, datetime.utcnow(), Chat(0, 0))
return _update
@pytest.fixture
def fee():
return MagicMock(return_value=0.0025)

View File

@ -283,7 +283,7 @@ def test_api__init__(default_conf, mocker):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock())
apiserver = ApiServer(default_conf)
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
@ -341,7 +341,7 @@ def test_api_run(default_conf, mocker, caplog):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
server_inst_mock = MagicMock()
server_inst_mock.run_in_thread = MagicMock()
@ -419,7 +419,7 @@ def test_api_cleanup(default_conf, mocker, caplog):
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
server_mock = MagicMock()
server_mock.cleanup = MagicMock()
@ -1877,7 +1877,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
"password": _TEST_PASS,
"ws_token": _TEST_WS_TOKEN
}})
mocker.patch('freqtrade.rpc.telegram.Updater')
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api')
apiserver = ApiServer(default_conf)
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))

File diff suppressed because it is too large Load Diff