-
-
Notifications
You must be signed in to change notification settings - Fork 39
/
bot.py
399 lines (303 loc) · 15.6 KB
/
bot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
import os
import asyncio
from typing import Final
import requests
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ContextTypes, ConversationHandler, filters
# Constants
BOT_USERNAME: Final = 'xyz'
BOT_TOKEN: Final = "Your Bot Token"
COINGECKO_API_URL: Final = "https://api.coingecko.com/api/v3"
# Conversation states
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 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',
'per_page': limit,
'page': 1,
'sparkline': False
})
if response.status_code == 200:
return response.json()
return []
def get_trending_cryptos():
response = requests.get(f"{COINGECKO_API_URL}/search/trending")
if response.status_code == 200:
return response.json().get('coins', [])
return []
def get_crypto_details(crypto_id: str, currency: str = 'usd'):
params = {'ids': crypto_id, 'vs_currencies': currency, 'include_24hr_change': 'true', 'include_market_cap': 'true'}
response = requests.get(f"{COINGECKO_API_URL}/simple/price", params=params)
if response.status_code == 200:
data = response.json()
return data.get(crypto_id)
return None
# COMMAND HANDLER FUNCTIONS
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await show_main_menu(update, context)
return MAIN_MENU
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
help_text = (
"Welcome to the Crypto Price Bot!\n\n"
"Commands:\n"
"/start - Show main menu\n"
"/help - Show this help message\n"
"/convert - Convert Currencies From One To Another\n\n"
"You can check prices of top cryptocurrencies, view trending coins, search for a specific cryptocurrency or Convert them."
)
await update.message.reply_text(help_text)
# 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')], # Added missing comma here
[InlineKeyboardButton("Quit", callback_data='quit')]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# 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 = []
for crypto in cryptos[i:i+2]:
crypto = crypto.get('item', crypto)
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"crypto:{crypto_id}"))
keyboard.append(row)
keyboard.append([InlineKeyboardButton("Back to Main Menu", callback_data='main_menu')])
reply_markup = InlineKeyboardMarkup(keyboard)
if update.callback_query:
await update.callback_query.edit_message_text(title, reply_markup=reply_markup)
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
]
keyboard.append([InlineKeyboardButton("Back to Main Menu", callback_data='main_menu')])
reply_markup = InlineKeyboardMarkup(keyboard)
await update.callback_query.edit_message_text('Choose a currency:', reply_markup=reply_markup)
async def button_click(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
if query.data == 'main_menu':
# 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
if query.data == 'top100':
await query.edit_message_text("Fetching top cryptocurrencies, please wait...")
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 COMPARE_SELECTION
elif query.data == 'compare_selection':
crypto_id = context.user_data.get('crypto')
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:")
# 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:
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:
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)
# 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', [])
if coins:
await show_crypto_list(update, context, coins[:10], "Search Results:")
return CHOOSING_CRYPTO
else:
await update.message.reply_text("Sorry, I couldn't find any cryptocurrency matching your search.")
await show_main_menu(update, context)
return MAIN_MENU
# Error Handler
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
print(f"Update {update} caused error {context.error}")
# Convert Currency into USD
# Function to fetch crypto price
def get_crypto_price(crypto: str, currency: str = 'usd'):
params = {'ids': crypto, 'vs_currencies': currency}
response = requests.get(COINGECKO_API_URL+'/simple/price', params=params)
data = response.json()
return data.get(crypto, {}).get(currency, 'Price not available')
# Convert crypto to different currencies
async def convert_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if len(context.args) < 3:
await update.message.reply_text("Please use the format: /convert <crypto> <currency> <amount>\n\nFor Example `/convert bitcoin usd 1` - Convert bitcoin price into usd by fettching real time data\n\n")
return
crypto = context.args[0].lower()
currency = context.args[1].lower()
amount = float(context.args[2])
price = get_crypto_price(crypto, currency)
if price != 'Price not available':
converted_amount = price * amount
await update.message.reply_text(f"{amount} {crypto.capitalize()} is worth {converted_amount} {currency.upper()}.")
else:
await update.message.reply_text('Price not available.')
# Add alerts
# Function to set up price alerts
user_alerts = {}
def set_price_alert(user_id, crypto, threshold_price, condition):
user_alerts[user_id] = (crypto, threshold_price, condition)
async def set_alert_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if len(context.args) < 3:
usage_text = (
"Please use the format: /setalert <crypto> <above|below> <price>\n"
)
await update.message.reply_text(usage_text)
return
crypto = context.args[0].lower() # Get the cryptocurrency (e.g., 'bitcoin')
condition = context.args[1].lower() # Get the condition (e.g., 'above' or 'below')
price = float(context.args[2]) # Get the price threshold
if condition not in ['above', 'below']:
await update.message.reply_text("Please specify 'above' or 'below' for the price alert condition.")
return
user_id = update.message.from_user.id # Get the user's ID
# Save the alert with the condition (above or below)
set_price_alert(user_id, crypto, price, condition)
# Notify the user that the alert has been set
await update.message.reply_text(
f"Price alert set for {crypto.capitalize()} when price is {condition} ${price} USD.\n"
"You'll be notified when this condition is met."
)
async def alert_check(context: ContextTypes.DEFAULT_TYPE):
for user_id, (crypto, threshold_price, condition) in user_alerts.items():
price = get_crypto_price(crypto)
# Check if the condition (above or below) is met
if (condition == 'above' and price >= threshold_price) or (condition == 'below' and price <= threshold_price):
await context.bot.send_message(
chat_id=user_id,
text=f"Price alert! {crypto.capitalize()} has {'exceeded' if condition == 'above' else 'dropped below'} ${threshold_price} USD. Current price: ${price} USD."
)
def main() -> None:
app = Application.builder().token(BOT_TOKEN).build()
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start)],
states={
MAIN_MENU: [CallbackQueryHandler(button_click)],
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
)
app.add_handler(conv_handler)
app.add_handler(CommandHandler('convert', convert_command))
app.add_handler(CommandHandler('setalert', set_alert_command))
app.add_handler(CommandHandler("help", help_command))
app.add_error_handler(error_handler)
# Run alert checker periodically
app.job_queue.run_repeating(alert_check, interval=60)
print('Starting bot...')
app.run_polling(poll_interval=3)
if __name__ == '__main__':
main()