management_commands.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. #!/usr/bin/env python3
  2. """
  3. Management Commands - Handles management and monitoring Telegram commands.
  4. """
  5. import logging
  6. import os
  7. import platform
  8. import sys
  9. from datetime import datetime, timedelta
  10. from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup
  11. from telegram.ext import ContextTypes
  12. import json
  13. from src.config.config import Config
  14. from src.monitoring.alarm_manager import AlarmManager
  15. from src.utils.token_display_formatter import get_formatter
  16. from src.stats import TradingStats
  17. from src.config.logging_config import LoggingManager
  18. logger = logging.getLogger(__name__)
  19. def _normalize_token_case(token: str) -> str:
  20. """
  21. Normalize token case: if any characters are already uppercase, keep as-is.
  22. Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
  23. """
  24. # Check if any character is already uppercase
  25. if any(c.isupper() for c in token):
  26. return token # Keep original case for mixed-case tokens
  27. else:
  28. return token.upper() # Convert to uppercase for all-lowercase input
  29. class ManagementCommands:
  30. """Handles all management-related Telegram commands."""
  31. def __init__(self, trading_engine, market_monitor):
  32. """Initialize with trading engine and market monitor."""
  33. self.trading_engine = trading_engine
  34. self.market_monitor = market_monitor
  35. self.alarm_manager = AlarmManager()
  36. def _is_authorized(self, chat_id: str) -> bool:
  37. """Check if the chat ID is authorized."""
  38. return str(chat_id) == str(Config.TELEGRAM_CHAT_ID)
  39. async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  40. """Handle the /monitoring command."""
  41. chat_id = update.effective_chat.id
  42. if not self._is_authorized(chat_id):
  43. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  44. return
  45. # Get alarm statistics
  46. alarm_stats = self.alarm_manager.get_statistics()
  47. # Get balance adjustments info
  48. stats = self.trading_engine.get_stats()
  49. adjustments_summary = stats.get_balance_adjustments_summary() if stats else {
  50. 'total_deposits': 0, 'total_withdrawals': 0, 'net_adjustment': 0, 'adjustment_count': 0
  51. }
  52. formatter = get_formatter()
  53. # Safety checks for monitoring attributes
  54. monitoring_active = self.market_monitor._monitoring_active
  55. status_text = f"""
  56. 🔄 <b>System Monitoring Status</b>
  57. 📊 <b>Order Monitoring:</b>
  58. • Active: {'✅ Yes' if monitoring_active else '❌ No'}
  59. • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
  60. • Market Monitor: {'✅ Running' if monitoring_active else '❌ Stopped'}
  61. 💰 <b>Balance Tracking:</b>
  62. • Total Adjustments: {adjustments_summary['adjustment_count']}
  63. • Net Adjustment: {await formatter.format_price_with_symbol(adjustments_summary['net_adjustment'])}
  64. 🔔 <b>Price Alarms:</b>
  65. • Active Alarms: {alarm_stats['total_active']}
  66. • Triggered Today: {alarm_stats['total_triggered']}
  67. • Tokens Monitored: {alarm_stats['tokens_tracked']}
  68. • Next Alarm ID: {alarm_stats['next_id']}
  69. 🔄 <b>External Trade Monitoring:</b>
  70. • Auto Stats Update: ✅ Enabled
  71. • External Notifications: ✅ Enabled
  72. 🛡️ <b>Risk Management:</b>
  73. • Automatic Stop Loss: {'✅ Enabled' if hasattr(Config, 'RISK_MANAGEMENT_ENABLED') and Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
  74. • Order-based Stop Loss: ✅ Enabled
  75. 📈 <b>Notifications:</b>
  76. • 🚀 Position Opened/Increased
  77. • 📉 Position Partially/Fully Closed
  78. • 🎯 P&L Calculations
  79. • 🔔 Price Alarm Triggers
  80. • 🔄 External Trade Detection
  81. • 🛑 Order-based Stop Loss Placement
  82. 💾 <b>Bot State Persistence:</b>
  83. • Trading Engine State: ✅ Saved
  84. • Order Tracking: ✅ Saved
  85. • State Survives Restarts: ✅ Yes
  86. ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
  87. 💡 <b>Monitoring Features:</b>
  88. • Real-time order fill detection
  89. • Automatic P&L calculation
  90. • Position change tracking
  91. • Price alarm monitoring
  92. • External trade monitoring
  93. • Auto stats synchronization
  94. • Order-based stop loss placement
  95. • Instant Telegram notifications
  96. """
  97. if alarm_stats['token_breakdown']:
  98. status_text += f"\n\n📋 <b>Active Alarms by Token:</b>\n"
  99. for token, count in alarm_stats['token_breakdown'].items():
  100. status_text += f"• {token}: {count} alarm{'s' if count != 1 else ''}\n"
  101. await context.bot.send_message(chat_id=chat_id, text=status_text.strip(), parse_mode='HTML')
  102. async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  103. """Handle the /alarm command."""
  104. chat_id = update.effective_chat.id
  105. if not self._is_authorized(chat_id):
  106. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  107. return
  108. try:
  109. if not context.args or len(context.args) == 0:
  110. # No arguments - list all alarms
  111. alarms = self.alarm_manager.get_all_active_alarms()
  112. message = self.alarm_manager.format_alarm_list(alarms)
  113. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  114. return
  115. elif len(context.args) == 1:
  116. arg = context.args[0]
  117. # Check if argument is a number (alarm ID to remove)
  118. try:
  119. alarm_id = int(arg)
  120. # Remove alarm by ID
  121. if self.alarm_manager.remove_alarm(alarm_id):
  122. await context.bot.send_message(chat_id=chat_id, text=f"✅ Alarm ID {alarm_id} has been removed.")
  123. else:
  124. await context.bot.send_message(chat_id=chat_id, text=f"❌ Alarm ID {alarm_id} not found.")
  125. return
  126. except ValueError:
  127. # Not a number, treat as token
  128. token = _normalize_token_case(arg)
  129. alarms = self.alarm_manager.get_alarms_by_token(token)
  130. message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms")
  131. await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML')
  132. return
  133. elif len(context.args) == 2:
  134. # Set new alarm: /alarm TOKEN PRICE
  135. token = _normalize_token_case(context.args[0])
  136. target_price = float(context.args[1])
  137. # Get current market price
  138. symbol = f"{token}/USDC:USDC"
  139. market_data = self.trading_engine.get_market_data(symbol)
  140. if not market_data or not market_data.get('ticker'):
  141. await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch current price for {token}")
  142. return
  143. current_price = float(market_data['ticker'].get('last', 0))
  144. if current_price <= 0:
  145. await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}")
  146. return
  147. # Create the alarm
  148. alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
  149. formatter = get_formatter()
  150. # Format confirmation message
  151. direction_emoji = "📈" if alarm['direction'] == 'above' else "📉"
  152. price_diff = abs(target_price - current_price)
  153. price_diff_percent = (price_diff / current_price) * 100 if current_price != 0 else 0
  154. target_price_str = await formatter.format_price_with_symbol(target_price, token)
  155. current_price_str = await formatter.format_price_with_symbol(current_price, token)
  156. price_diff_str = await formatter.format_price_with_symbol(price_diff, token)
  157. message = f"""
  158. ✅ <b>Price Alarm Created</b>
  159. 📊 <b>Alarm Details:</b>
  160. • Alarm ID: {alarm['id']}
  161. • Token: {token}
  162. • Target Price: {target_price_str}
  163. • Current Price: {current_price_str}
  164. • Direction: {alarm['direction'].upper()}
  165. {direction_emoji} <b>Alert Condition:</b>
  166. Will trigger when {token} price moves {alarm['direction']} {target_price_str}
  167. 💰 <b>Price Difference:</b>
  168. • Distance: {price_diff_str} ({price_diff_percent:.2f}%)
  169. • Status: ACTIVE ✅
  170. ⏰ <b>Created:</b> {datetime.now().strftime('%H:%M:%S')}
  171. 💡 The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered.
  172. """
  173. await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML')
  174. else:
  175. # Too many arguments
  176. await context.bot.send_message(chat_id=chat_id, text=(
  177. "❌ Invalid usage. Examples:\n\n"
  178. "• <code>/alarm</code> - List all alarms\n"
  179. "• <code>/alarm BTC</code> - List BTC alarms\n"
  180. "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
  181. "• <code>/alarm 3</code> - Remove alarm ID 3"
  182. ), parse_mode='HTML')
  183. except ValueError:
  184. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.")
  185. except Exception as e:
  186. error_message = f"❌ Error processing alarm command: {str(e)}"
  187. await context.bot.send_message(chat_id=chat_id, text=error_message)
  188. logger.error(f"Error in alarm command: {e}")
  189. async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  190. """Handle the /logs command."""
  191. chat_id = update.effective_chat.id
  192. if not self._is_authorized(chat_id):
  193. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  194. return
  195. try:
  196. logs_dir = "logs"
  197. if not os.path.exists(logs_dir):
  198. await context.bot.send_message(chat_id=chat_id, text="📜 No logs directory found.")
  199. return
  200. # Get log files
  201. log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
  202. if not log_files:
  203. await context.bot.send_message(chat_id=chat_id, text="📜 No log files found.")
  204. return
  205. # Handle cleanup command
  206. if context.args and context.args[0].lower() == 'cleanup':
  207. days_to_keep = 30 # Default
  208. if len(context.args) > 1:
  209. try:
  210. days_to_keep = int(context.args[1])
  211. except ValueError:
  212. await context.bot.send_message(chat_id=chat_id, text="❌ Invalid number of days. Using default (30 days).")
  213. # Clean up old log files
  214. cutoff_date = datetime.now() - timedelta(days=days_to_keep)
  215. cleaned_files = 0
  216. total_size_cleaned = 0
  217. for log_file in log_files:
  218. file_path = os.path.join(logs_dir, log_file)
  219. file_stat = os.stat(file_path)
  220. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  221. if file_date < cutoff_date:
  222. file_size = file_stat.st_size
  223. os.remove(file_path)
  224. cleaned_files += 1
  225. total_size_cleaned += file_size
  226. size_cleaned_mb = total_size_cleaned / (1024 * 1024)
  227. await context.bot.send_message(chat_id=chat_id, text=
  228. f"🧹 Log cleanup complete.\n"
  229. f"• Files older than {days_to_keep} days removed.\n"
  230. f"• Total files deleted: {cleaned_files}\n"
  231. f"• Total size cleaned: {size_cleaned_mb:.2f} MB"
  232. )
  233. return
  234. # Show log statistics
  235. total_size = 0
  236. oldest_file = None
  237. newest_file = None
  238. recent_files = []
  239. for log_file in sorted(log_files):
  240. file_path = os.path.join(logs_dir, log_file)
  241. file_stat = os.stat(file_path)
  242. file_size = file_stat.st_size
  243. file_date = datetime.fromtimestamp(file_stat.st_mtime)
  244. total_size += file_size
  245. if oldest_file is None or file_date < oldest_file[1]:
  246. oldest_file = (log_file, file_date)
  247. if newest_file is None or file_date > newest_file[1]:
  248. newest_file = (log_file, file_date)
  249. # Keep track of recent files
  250. if len(recent_files) < 5:
  251. recent_files.append((log_file, file_size, file_date))
  252. logs_message = f"""
  253. 📜 <b>System Logging Status</b>
  254. 📁 <b>Log Directory:</b> {logs_dir}/
  255. • Total Files: {len(log_files)}
  256. • Total Size: {total_size / 1024 / 1024:.2f} MB
  257. • Oldest File: {oldest_file[0]} ({oldest_file[1].strftime('%m/%d/%Y')})
  258. • Newest File: {newest_file[0]} ({newest_file[1].strftime('%m/%d/%Y')})
  259. 📋 <b>Recent Log Files:</b>
  260. """
  261. for log_file, file_size, file_date in reversed(recent_files):
  262. size_mb = file_size / 1024 / 1024
  263. logs_message += f"• {log_file} ({size_mb:.2f} MB) - {file_date.strftime('%m/%d %H:%M')}\n"
  264. logs_message += f"""
  265. 📊 <b>Log Management:</b>
  266. • Location: ./logs/
  267. • Rotation: Daily
  268. • Retention: Manual cleanup available
  269. • Format: timestamp - module - level - message
  270. 🧹 <b>Cleanup Commands:</b>
  271. • <code>/logs cleanup</code> - Remove logs older than 30 days
  272. • <code>/logs cleanup 7</code> - Remove logs older than 7 days
  273. 💡 <b>Log Levels:</b>
  274. • INFO: Normal operations
  275. • ERROR: Error conditions
  276. • DEBUG: Detailed debugging (if enabled)
  277. """
  278. await context.bot.send_message(chat_id=chat_id, text=logs_message.strip(), parse_mode='HTML')
  279. except Exception as e:
  280. error_message = f"❌ Error processing logs command: {str(e)}"
  281. await context.bot.send_message(chat_id=chat_id, text=error_message)
  282. logger.error(f"Error in logs command: {e}")
  283. async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  284. """Handle the /debug command."""
  285. chat_id = update.effective_chat.id
  286. if not self._is_authorized(chat_id):
  287. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  288. return
  289. try:
  290. # Get system information
  291. debug_info = f"""
  292. 🐛 <b>Debug Information</b>
  293. 💻 <b>System Info:</b>
  294. • Python: {sys.version.split()[0]}
  295. • Platform: {platform.system()} {platform.release()}
  296. • Architecture: {platform.machine()}
  297. 📊 <b>Trading Engine:</b>
  298. • Stats Available: {'✅ Yes' if self.trading_engine.get_stats() else '❌ No'}
  299. • Client Connected: {'✅ Yes' if self.trading_engine.client else '❌ No'}
  300. 🔄 <b>Market Monitor:</b>
  301. • Running: {'✅ Yes' if self.market_monitor._monitoring_active else '❌ No'}
  302. 📁 <b>State Files:</b>
  303. • Price Alarms: {'✅ Exists' if os.path.exists('data/price_alarms.json') else '❌ Missing'}
  304. • Trading Stats: {'✅ Exists' if os.path.exists('data/trading_stats.sqlite') else '❌ Missing'}
  305. 🔔 <b>Alarm Manager:</b>
  306. • Active Alarms: {self.alarm_manager.get_statistics()['total_active']}
  307. • Triggered Alarms: {self.alarm_manager.get_statistics()['total_triggered']}
  308. ⏰ <b>Timestamps:</b>
  309. • Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  310. • Debug Generated: {datetime.now().isoformat()}
  311. """
  312. # Get current positions for debugging
  313. try:
  314. positions = self.trading_engine.get_positions()
  315. if positions:
  316. debug_info += f"\n📈 <b>Current Positions:</b> {len(positions)} found\n"
  317. for pos in positions[:3]: # Show first 3 positions
  318. symbol = pos.get('symbol', 'Unknown').replace('/USDC:USDC', '')
  319. contracts = pos.get('contracts', 0)
  320. if float(contracts) != 0:
  321. debug_info += f" • {symbol}: {contracts} contracts\n"
  322. else:
  323. debug_info += "\n📈 <b>Positions:</b> No positions found\n"
  324. except Exception as e:
  325. debug_info += f"\n📈 <b>Positions:</b> Error fetching ({str(e)})\n"
  326. # Get balance for debugging
  327. try:
  328. balance = self.trading_engine.get_balance()
  329. if balance and balance.get('total'):
  330. usdc_balance = float(balance['total'].get('USDC', 0))
  331. debug_info += f"\n💰 <b>USDC Balance:</b> ${usdc_balance:,.2f}\n"
  332. else:
  333. debug_info += "\n💰 <b>Balance:</b> No balance data\n"
  334. except Exception as e:
  335. debug_info += f"\n💰 <b>Balance:</b> Error fetching ({str(e)})\n"
  336. await context.bot.send_message(chat_id=chat_id, text=debug_info.strip(), parse_mode='HTML')
  337. except Exception as e:
  338. logger.error(f"❌ Error in debug command: {e}")
  339. await context.bot.send_message(chat_id=chat_id, text=f"❌ Debug error: {e}")
  340. async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  341. """Handle the /version command."""
  342. chat_id = update.effective_chat.id
  343. if not self._is_authorized(chat_id):
  344. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  345. return
  346. try:
  347. # Get uptime info
  348. uptime_info = "Unknown"
  349. try:
  350. import psutil
  351. process = psutil.Process()
  352. create_time = datetime.fromtimestamp(process.create_time())
  353. uptime = datetime.now() - create_time
  354. days = uptime.days
  355. hours, remainder = divmod(uptime.seconds, 3600)
  356. minutes, _ = divmod(remainder, 60)
  357. uptime_info = f"{days}d {hours}h {minutes}m"
  358. except ImportError:
  359. pass
  360. # Get stats info
  361. stats = self.trading_engine.get_stats()
  362. if stats:
  363. basic_stats = stats.get_basic_stats()
  364. else:
  365. basic_stats = {'total_trades': 0, 'completed_trades': 0, 'days_active': 0, 'start_date': 'Unknown'}
  366. version_text = f"""
  367. 🤖 <b>Trading Bot Version & System Info</b>
  368. 📱 <b>Bot Information:</b>
  369. • Version: <code>2.1.2</code>
  370. • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
  371. • Uptime: {uptime_info}
  372. • Default Token: {Config.DEFAULT_TRADING_TOKEN}
  373. 💻 <b>System Information:</b>
  374. • Python: {sys.version.split()[0]}
  375. • Platform: {platform.system()} {platform.release()}
  376. • Architecture: {platform.machine()}
  377. 📊 <b>Trading Stats:</b>
  378. • Total Orders: {basic_stats['total_trades']}
  379. • Completed Trades: {basic_stats['completed_trades']}
  380. • Days Active: {basic_stats['days_active']}
  381. • Start Date: {basic_stats['start_date']}
  382. 🔄 <b>Monitoring Status:</b>
  383. • Market Monitor: {'✅ Active' if self.market_monitor._monitoring_active else '❌ Inactive'}
  384. • External Trades: ✅ Active
  385. • Price Alarms: ✅ Active ({self.alarm_manager.get_statistics()['total_active']} active)
  386. ⏰ <b>Current Time:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  387. """
  388. await context.bot.send_message(chat_id=chat_id, text=version_text.strip(), parse_mode='HTML')
  389. except Exception as e:
  390. error_message = f"❌ Error processing version command: {str(e)}"
  391. await context.bot.send_message(chat_id=chat_id, text=error_message)
  392. logger.error(f"Error in version command: {e}")
  393. async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  394. """Handle the /keyboard command to enable/show custom keyboard."""
  395. chat_id = update.effective_chat.id
  396. if not self._is_authorized(chat_id):
  397. await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
  398. return
  399. keyboard_enabled = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_ENABLED', False)
  400. keyboard_layout_str = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', '')
  401. if keyboard_enabled and keyboard_layout_str:
  402. rows_str = keyboard_layout_str.split('|')
  403. keyboard = []
  404. button_details_message = ""
  405. for row_str in rows_str:
  406. button_commands = row_str.split(',')
  407. row_buttons = []
  408. for cmd_text in button_commands:
  409. cmd_text = cmd_text.strip()
  410. if cmd_text: # Ensure not empty after strip
  411. row_buttons.append(KeyboardButton(cmd_text))
  412. # For the message, show the command and a brief description if possible
  413. # This part would require a mapping from command to description
  414. # For now, just list the command text.
  415. button_details_message += f"• {cmd_text}\n"
  416. if row_buttons:
  417. keyboard.append(row_buttons)
  418. if not keyboard:
  419. await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard layout is empty or invalid in config.", parse_mode='HTML')
  420. return
  421. reply_markup = ReplyKeyboardMarkup(
  422. keyboard,
  423. resize_keyboard=True,
  424. one_time_keyboard=False, # Persistent keyboard
  425. selective=True
  426. )
  427. message_text = f"⌨️ <b>Custom Keyboard Activated!</b>\n\n🎯 <b>Your quick buttons based on config are ready:</b>\n{button_details_message}\n💡 <b>How to use:</b>\nTap any button below to send the command instantly!\n\n🔧 These buttons will stay at the bottom of your chat."
  428. await context.bot.send_message(chat_id=chat_id, text=message_text, reply_markup=reply_markup, parse_mode='HTML')
  429. elif not keyboard_enabled:
  430. await context.bot.send_message(chat_id=chat_id, text="❌ <b>Custom Keyboard Disabled</b>\n\n🔧 <b>To enable:</b>\n• Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n• Restart the bot\n• Run /keyboard again\n\n📋 <b>Current config:</b>\n• Enabled: {keyboard_enabled}", parse_mode='HTML')
  431. else: # keyboard_enabled is true, but layout_str is empty
  432. await context.bot.send_message(chat_id=chat_id, text="⚠️ Custom keyboard is enabled but the layout string (TELEGRAM_CUSTOM_KEYBOARD_LAYOUT) is empty in your configuration. Please define a layout.", parse_mode='HTML')