diff --git a/bot.py b/bot.py index 443a0f8..45c4fdb 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,5 @@ import os +import asyncio from typing import Final import requests from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup @@ -10,13 +11,18 @@ COINGECKO_API_URL: Final = "https://api.coingecko.com/api/v3" # Conversation states -MAIN_MENU, CHOOSING_CRYPTO, CHOOSING_CURRENCY, TYPING_SEARCH = range(4) +MAIN_MENU, CHOOSING_CRYPTO, CHOOSING_CURRENCY, TYPING_SEARCH, COMPARE_SELECTION = range(5) # Supported currencies SUPPORTED_CURRENCIES = ['usd', 'eur', 'gbp', 'jpy', 'aud', 'cad', 'chf', 'cny', 'inr'] -# API Functions -def get_top_cryptos(limit=100): + + +# API HELPER FUNCTIONS + +# def get_top_cryptos(limit=100): +def get_top_cryptos(is_comparing=False,limit=100): + response = requests.get(f"{COINGECKO_API_URL}/coins/markets", params={ 'vs_currency': 'usd', 'order': 'market_cap_desc', @@ -28,6 +34,8 @@ def get_top_cryptos(limit=100): return response.json() return [] + + def get_trending_cryptos(): response = requests.get(f"{COINGECKO_API_URL}/search/trending") if response.status_code == 200: @@ -42,7 +50,8 @@ def get_crypto_details(crypto_id: str, currency: str = 'usd'): return data.get(crypto_id) return None -# Command Handlers + +# COMMAND HANDLER FUNCTIONS async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: await show_main_menu(update, context) return MAIN_MENU @@ -57,22 +66,33 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No ) await update.message.reply_text(help_text) -# Menu Functions -async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + + +# Menu Display and Button Handlers +async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, is_comparing: bool = False) -> None: + keyboard = [ [InlineKeyboardButton("Top 100 Cryptocurrencies", callback_data='top100')], [InlineKeyboardButton("Trending Cryptocurrencies", callback_data='trending')], - [InlineKeyboardButton("Search Cryptocurrency", callback_data='search')] + [InlineKeyboardButton("Search Cryptocurrency", callback_data='search')], # Added missing comma here + [InlineKeyboardButton("Quit", callback_data='quit')] ] reply_markup = InlineKeyboardMarkup(keyboard) - text = "Welcome to the Crypto Price Bot! What would you like to do?" - + + # Show welcome message only when not comparing + if not is_comparing: + text = "Welcome to the Crypto Price Bot! What would you like to do?" + else: + text = "Select a cryptocurrency to compare." + if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await update.message.reply_text(text, reply_markup=reply_markup) + async def show_crypto_list(update: Update, context: ContextTypes.DEFAULT_TYPE, cryptos, title) -> None: + keyboard = [] for i in range(0, len(cryptos), 2): row = [] @@ -91,7 +111,49 @@ async def show_crypto_list(update: Update, context: ContextTypes.DEFAULT_TYPE, c else: await update.message.reply_text(title, reply_markup=reply_markup) +async def show_crypto_details(update: Update, context: ContextTypes.DEFAULT_TYPE, crypto_id: str, currency: str) -> None: + await asyncio.sleep(1) # Add a delay to avoid hitting rate limits + details = get_crypto_details(crypto_id, currency) + if details: + price = details.get(currency, 'N/A') + change_24h = details.get(f'{currency}_24h_change', 'N/A') + market_cap = details.get(f'{currency}_market_cap', 'N/A') + trading_volume = details.get(f'{currency}_24h_vol', 'N/A') + + try: + change_24h_float = float(change_24h) + change_symbol = '🔺' if change_24h_float > 0 else '🔻' if change_24h_float < 0 else '➖' + except (ValueError, TypeError): + change_symbol = '➖' + + message = ( + f"💰 {crypto_id.capitalize()} ({currency.upper()})\n" + f"Price: {price} {currency.upper()}\n" + f"24h Change: {change_symbol} {change_24h}%\n" + f"Market Cap: {market_cap} {currency.upper()}\n" + f"24h Trading Volume: {trading_volume} {currency.upper()}\n\n" + ) + + # Check if the new message is different from the current message + current_message = update.callback_query.message.text + if message != current_message: + await update.callback_query.edit_message_text(message) + + # Adding options: Compare with other cryptos or Main Menu + keyboard = [ + [InlineKeyboardButton("Compare with another Cryptocurrency", callback_data='compare_selection')], + [InlineKeyboardButton("Main Menu", callback_data='main_menu')], + [InlineKeyboardButton("Quit", callback_data='quit')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.callback_query.message.reply_text("Select an option:", reply_markup=reply_markup) + else: + await update.callback_query.edit_message_text("🚫 Unable to retrieve cryptocurrency details.") + + + async def show_currency_options(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + keyboard = [ [InlineKeyboardButton(currency.upper(), callback_data=f"currency:{currency}")] for currency in SUPPORTED_CURRENCIES @@ -100,60 +162,120 @@ async def show_currency_options(update: Update, context: ContextTypes.DEFAULT_TY reply_markup = InlineKeyboardMarkup(keyboard) await update.callback_query.edit_message_text('Choose a currency:', reply_markup=reply_markup) -# Callback Query Handler -async def button_click(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + + +async def button_click(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() if query.data == 'main_menu': - await show_main_menu(update, context) + # Instead of deleting the message, just edit it to show the main menu + try: + await show_main_menu(update, context) + except Exception as e: + print(f"Error displaying main menu: {e}") + return MAIN_MENU - elif query.data == 'top100': + + if query.data == 'top100': await query.edit_message_text("Fetching top cryptocurrencies, please wait...") - cryptos = get_top_cryptos() + cryptos = get_top_cryptos() # No arguments passed await show_crypto_list(update, context, cryptos, "Top 100 Cryptocurrencies:") return CHOOSING_CRYPTO + elif query.data == 'quit': + # Quit the current conversation and show a message + await query.edit_message_text("You can return to the main menu anytime by using /start.") + + return MAIN_MENU # This will allow the user to start again later + + elif query.data == 'trending': await query.edit_message_text("Fetching trending cryptocurrencies, please wait...") cryptos = get_trending_cryptos() await show_crypto_list(update, context, cryptos, "Trending Cryptocurrencies:") + return CHOOSING_CRYPTO elif query.data == 'search': await query.edit_message_text("Please enter the name of the cryptocurrency you want to check:") + return TYPING_SEARCH elif query.data.startswith('crypto:'): context.user_data['crypto'] = query.data.split(':')[1] await show_currency_options(update, context) + return CHOOSING_CURRENCY elif query.data.startswith('currency:'): currency = query.data.split(':')[1] crypto_id = context.user_data.get('crypto', 'bitcoin') await show_crypto_details(update, context, crypto_id, currency) - return MAIN_MENU + + return COMPARE_SELECTION + elif query.data == 'compare_selection': + crypto_id = context.user_data.get('crypto') -async def show_crypto_details(update: Update, context: ContextTypes.DEFAULT_TYPE, crypto_id: str, currency: str) -> None: - details = get_crypto_details(crypto_id, currency) - if details: - price = details.get(currency, 'N/A') - change_24h = details.get(f'{currency}_24h_change', 'N/A') - market_cap = details.get(f'{currency}_market_cap', 'N/A') + if not crypto_id: + await query.edit_message_text("Please select a cryptocurrency before comparing.") + return + + # Fetch the top 100 currencies again for selection + await query.edit_message_text("Fetching top 100 currencies for comparison, please wait...") + cryptos = get_top_cryptos(is_comparing=True) # Fetch top 100 for comparison + await show_crypto_list(update, context, cryptos, f"Compare {crypto_id} with another currency:") - change_symbol = '🔺' if change_24h > 0 else '🔻' if change_24h < 0 else '➖' - message = ( - f"💰 {crypto_id.capitalize()} ({currency.upper()})\n" - f"Price: {price:,.2f} {currency.upper()}\n" - f"24h Change: {change_symbol} {abs(change_24h):.2f}%\n" - f"Market Cap: {market_cap:,.0f} {currency.upper()}" - ) + # Now wait for the user to select a new currency to compare + return CHOOSING_CURRENCY + + elif query.data == 'cancel_compare': + await query.edit_message_text("Comparison cancelled.") + else: - message = f"Sorry, I couldn't find the details for {crypto_id}." + await query.edit_message_text("Invalid selection. Returning to main menu.") + await show_main_menu(update, context) + + return MAIN_MENU + + + + +# **New Function to Show Comparison Options** +# Updated show_compare_options function +async def show_compare_options(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - keyboard = [[InlineKeyboardButton("Back to Main Menu", callback_data='main_menu')]] + cryptos = get_top_cryptos() # Fetch the top 100 cryptocurrencies + + keyboard = [] + for i in range(0, len(cryptos), 2): + row = [] + for crypto in cryptos[i:i + 2]: + name = crypto.get('name', 'Unknown') + symbol = crypto.get('symbol', 'Unknown') + crypto_id = crypto.get('id', 'unknown') + row.append(InlineKeyboardButton(f"{name} ({symbol.upper()})", callback_data=f"compare:{crypto_id}")) + keyboard.append(row) + + keyboard.append([InlineKeyboardButton("Cancel", callback_data='cancel_compare')]) reply_markup = InlineKeyboardMarkup(keyboard) - await update.callback_query.edit_message_text(message, reply_markup=reply_markup) -# Message Handler + # Skip the welcome message during comparison + await show_main_menu(update, context, is_comparing=True) + + + +# **New Function to Handle Comparison Prompt** +async def compare_prompt_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + # Send the message asking user to select another cryptocurrency + await update.callback_query.message.reply_text("Select the another Cryptocurrency...") + + # Then proceed to show the comparison options + await show_compare_options(update, context) + return COMPARE_SELECTION + + +# 6. Message and Error Handlers async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + user_input = update.message.text.lower() search_results = requests.get(f"{COINGECKO_API_URL}/search", params={'query': user_input}).json() coins = search_results.get('coins', []) @@ -167,9 +289,11 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> return MAIN_MENU # Error Handler -async def error(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + print(f"Update {update} caused error {context.error}") + def main() -> None: app = Application.builder().token(BOT_TOKEN).build() @@ -180,6 +304,7 @@ def main() -> None: CHOOSING_CRYPTO: [CallbackQueryHandler(button_click)], CHOOSING_CURRENCY: [CallbackQueryHandler(button_click)], TYPING_SEARCH: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)], + COMPARE_SELECTION: [CallbackQueryHandler(button_click)] # Added COMPARE_SELECTION }, fallbacks=[CommandHandler("start", start)], per_message=False @@ -187,7 +312,7 @@ def main() -> None: app.add_handler(conv_handler) app.add_handler(CommandHandler("help", help_command)) - app.add_error_handler(error) + app.add_error_handler(error_handler) print('Starting bot...') app.run_polling(poll_interval=3)