-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathtelegram-digest.py
More file actions
391 lines (303 loc) · 13.8 KB
/
telegram-digest.py
File metadata and controls
391 lines (303 loc) · 13.8 KB
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
#!/usr/bin/env python3
"""
Crypto News Telegram Digest Bot
A Telegram bot that sends scheduled news digests and on-demand news updates.
Features:
- /news - Get latest news
- /bitcoin - Bitcoin-specific news
- /defi - DeFi news
- /breaking - Breaking news
- /trending - Trending topics
- /digest - Full digest with analysis
- /subscribe - Subscribe to daily digests
- /unsubscribe - Unsubscribe from digests
Setup:
1. Create a bot with @BotFather on Telegram
2. Get your bot token
3. Set BOT_TOKEN environment variable
4. Run: python telegram-digest.py
Requirements:
pip install python-telegram-bot aiohttp
"""
import os
import json
import asyncio
import logging
from datetime import datetime, time
from typing import Optional
import aiohttp
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
JobQueue,
)
# Configuration
BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN_HERE')
API_BASE = 'https://free-crypto-news.vercel.app'
# Storage for subscribed users (use database in production)
subscribed_users: dict[int, dict] = {}
# Logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
async def fetch_news(endpoint: str, limit: int = 5) -> Optional[dict]:
"""Fetch news from API."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f'{API_BASE}/api/{endpoint}?limit={limit}') as resp:
if resp.status == 200:
return await resp.json()
except Exception as e:
logger.error(f"API fetch error: {e}")
return None
def format_article(article: dict, index: int = None) -> str:
"""Format a single article for Telegram."""
prefix = f"{index}. " if index else "📰 "
source = article.get('source', 'Unknown')
title = article.get('title', 'No title')
link = article.get('link', '')
time_ago = article.get('timeAgo', '')
return f"{prefix}*{escape_markdown(title)}*\n└ {source} • {time_ago}\n🔗 [Read more]({link})"
def escape_markdown(text: str) -> str:
"""Escape markdown special characters."""
special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for char in special_chars:
text = text.replace(char, f'\\{char}')
return text
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send welcome message."""
keyboard = [
[
InlineKeyboardButton("📰 Latest News", callback_data='news'),
InlineKeyboardButton("🔥 Breaking", callback_data='breaking'),
],
[
InlineKeyboardButton("₿ Bitcoin", callback_data='bitcoin'),
InlineKeyboardButton("🏦 DeFi", callback_data='defi'),
],
[
InlineKeyboardButton("📊 Trending", callback_data='trending'),
InlineKeyboardButton("📋 Full Digest", callback_data='digest'),
],
[
InlineKeyboardButton("🔔 Subscribe Daily", callback_data='subscribe'),
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
welcome_text = """
🚀 *Welcome to Crypto News Bot\\!*
Get real\\-time crypto news from 7 major sources:
• CoinDesk • The Block • Decrypt
• CoinTelegraph • Bitcoin Magazine
• Blockworks • The Defiant
*Commands:*
/news \\- Latest news
/bitcoin \\- Bitcoin news
/defi \\- DeFi news
/breaking \\- Breaking \\(last 2h\\)
/trending \\- Trending topics
/digest \\- Full analysis digest
/subscribe \\- Daily digest
/unsubscribe \\- Stop daily digest
Choose an option below or type a command:
"""
await update.message.reply_text(
welcome_text,
reply_markup=reply_markup,
parse_mode='MarkdownV2'
)
async def news_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Get latest news."""
await send_news(update, 'news', '📰 Latest Crypto News', 5)
async def bitcoin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Get Bitcoin news."""
await send_news(update, 'bitcoin', '₿ Bitcoin News', 5)
async def defi_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Get DeFi news."""
await send_news(update, 'defi', '🏦 DeFi News', 5)
async def breaking_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Get breaking news."""
await send_news(update, 'breaking', '🔥 Breaking News', 5)
async def send_news(update: Update, endpoint: str, title: str, limit: int) -> None:
"""Generic news sender."""
message = update.message or update.callback_query.message
# Send loading message
loading_msg = await message.reply_text("⏳ Fetching news...")
data = await fetch_news(endpoint, limit)
if not data or not data.get('articles'):
await loading_msg.edit_text("❌ Failed to fetch news. Try again later.")
return
articles = data['articles'][:limit]
text = f"*{escape_markdown(title)}*\n\n"
for i, article in enumerate(articles, 1):
text += format_article(article, i) + "\n\n"
text += f"_Updated: {datetime.now().strftime('%H:%M UTC')}_"
await loading_msg.edit_text(text, parse_mode='MarkdownV2', disable_web_page_preview=True)
async def trending_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Get trending topics."""
message = update.message or update.callback_query.message
loading_msg = await message.reply_text("⏳ Analyzing trends...")
data = await fetch_news('trending', 10)
if not data or not data.get('trending'):
await loading_msg.edit_text("❌ Failed to fetch trends.")
return
text = "*📊 Trending Crypto Topics \\(24h\\)*\n\n"
for i, topic in enumerate(data['trending'][:10], 1):
sentiment_emoji = '🟢' if topic.get('sentiment') == 'bullish' else '🔴' if topic.get('sentiment') == 'bearish' else '⚪'
topic_name = escape_markdown(topic.get('topic', 'Unknown'))
count = topic.get('count', 0)
sentiment = topic.get('sentiment', 'neutral')
text += f"{i}\\. {sentiment_emoji} *{topic_name}* \\- {count} mentions \\({sentiment}\\)\n"
text += f"\n_Analyzed {data.get('articlesAnalyzed', 0)} articles_"
await loading_msg.edit_text(text, parse_mode='MarkdownV2')
async def digest_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send full digest with analysis."""
message = update.message or update.callback_query.message
loading_msg = await message.reply_text("⏳ Generating digest... This may take a moment.")
# Fetch multiple endpoints
news_data = await fetch_news('news', 5)
trending_data = await fetch_news('trending', 5)
analyze_data = await fetch_news('analyze', 10)
text = "📋 *CRYPTO NEWS DIGEST*\n"
text += f"_{escape_markdown(datetime.now().strftime('%B %d, %Y'))}_\n\n"
# Market Sentiment
if analyze_data and analyze_data.get('analysis'):
analysis = analyze_data['analysis']
sentiment = analysis.get('overallSentiment', 'neutral')
breakdown = analysis.get('sentimentBreakdown', {})
sentiment_emoji = '🟢' if sentiment == 'bullish' else '🔴' if sentiment == 'bearish' else '⚪'
text += f"*Market Sentiment:* {sentiment_emoji} {escape_markdown(sentiment.upper())}\n"
text += f"Bullish: {breakdown.get('bullish', 0)} \\| Bearish: {breakdown.get('bearish', 0)} \\| Neutral: {breakdown.get('neutral', 0)}\n\n"
# Trending Topics
if trending_data and trending_data.get('trending'):
text += "*🔥 Top Trending:*\n"
for topic in trending_data['trending'][:5]:
emoji = '🟢' if topic.get('sentiment') == 'bullish' else '🔴' if topic.get('sentiment') == 'bearish' else '⚪'
text += f" {emoji} {escape_markdown(topic.get('topic', ''))} \\({topic.get('count', 0)}\\)\n"
text += "\n"
# Top Headlines
if news_data and news_data.get('articles'):
text += "*📰 Top Headlines:*\n\n"
for i, article in enumerate(news_data['articles'][:5], 1):
text += format_article(article, i) + "\n\n"
text += "_Powered by Free Crypto News API_"
await loading_msg.edit_text(text, parse_mode='MarkdownV2', disable_web_page_preview=True)
async def subscribe_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Subscribe to daily digests."""
user_id = update.effective_user.id
chat_id = update.effective_chat.id
if user_id in subscribed_users:
await update.message.reply_text(
"✅ You're already subscribed to daily digests\\!\n"
"Use /unsubscribe to stop receiving them\\.",
parse_mode='MarkdownV2'
)
return
subscribed_users[user_id] = {
'chat_id': chat_id,
'subscribed_at': datetime.now().isoformat(),
'timezone': 'UTC'
}
await update.message.reply_text(
"🔔 *Subscribed to Daily Digest\\!*\n\n"
"You'll receive a news digest every day at 9:00 AM UTC\\.\n"
"Use /unsubscribe to stop receiving digests\\.",
parse_mode='MarkdownV2'
)
async def unsubscribe_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Unsubscribe from daily digests."""
user_id = update.effective_user.id
if user_id in subscribed_users:
del subscribed_users[user_id]
await update.message.reply_text("🔕 Unsubscribed from daily digests\\.", parse_mode='MarkdownV2')
else:
await update.message.reply_text("You're not subscribed to daily digests\\.", parse_mode='MarkdownV2')
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle button callbacks."""
query = update.callback_query
await query.answer()
action = query.data
if action == 'news':
await send_news(update, 'news', '📰 Latest Crypto News', 5)
elif action == 'bitcoin':
await send_news(update, 'bitcoin', '₿ Bitcoin News', 5)
elif action == 'defi':
await send_news(update, 'defi', '🏦 DeFi News', 5)
elif action == 'breaking':
await send_news(update, 'breaking', '🔥 Breaking News', 5)
elif action == 'trending':
await trending_command(update, context)
elif action == 'digest':
await digest_command(update, context)
elif action == 'subscribe':
# Simulate message for subscribe
update.message = query.message
await subscribe_command(update, context)
async def send_daily_digest(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send daily digest to all subscribers."""
logger.info(f"Sending daily digest to {len(subscribed_users)} subscribers")
for user_id, user_data in subscribed_users.items():
try:
chat_id = user_data['chat_id']
# Fetch digest data
news_data = await fetch_news('news', 5)
trending_data = await fetch_news('trending', 5)
text = "🌅 *GOOD MORNING\\! Your Daily Crypto Digest*\n\n"
if trending_data and trending_data.get('trending'):
text += "*🔥 Today's Hot Topics:*\n"
for topic in trending_data['trending'][:5]:
emoji = '🟢' if topic.get('sentiment') == 'bullish' else '🔴' if topic.get('sentiment') == 'bearish' else '⚪'
text += f" {emoji} {escape_markdown(topic.get('topic', ''))}\n"
text += "\n"
if news_data and news_data.get('articles'):
text += "*📰 Headlines:*\n\n"
for i, article in enumerate(news_data['articles'][:5], 1):
text += format_article(article, i) + "\n\n"
text += "_Have a great day\\! 🚀_"
await context.bot.send_message(
chat_id=chat_id,
text=text,
parse_mode='MarkdownV2',
disable_web_page_preview=True
)
except Exception as e:
logger.error(f"Failed to send digest to {user_id}: {e}")
def main() -> None:
"""Start the bot."""
if BOT_TOKEN == 'YOUR_BOT_TOKEN_HERE':
print("❌ Please set TELEGRAM_BOT_TOKEN environment variable")
print(" Get a token from @BotFather on Telegram")
return
# Create application
application = Application.builder().token(BOT_TOKEN).build()
# Add handlers
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", start))
application.add_handler(CommandHandler("news", news_command))
application.add_handler(CommandHandler("bitcoin", bitcoin_command))
application.add_handler(CommandHandler("defi", defi_command))
application.add_handler(CommandHandler("breaking", breaking_command))
application.add_handler(CommandHandler("trending", trending_command))
application.add_handler(CommandHandler("digest", digest_command))
application.add_handler(CommandHandler("subscribe", subscribe_command))
application.add_handler(CommandHandler("unsubscribe", unsubscribe_command))
application.add_handler(CallbackQueryHandler(button_callback))
# Schedule daily digest at 9:00 AM UTC
job_queue = application.job_queue
if job_queue:
job_queue.run_daily(
send_daily_digest,
time=time(hour=9, minute=0),
name='daily_digest'
)
# Start bot
print("🤖 Crypto News Telegram Bot starting...")
print(" Press Ctrl+C to stop")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == '__main__':
main()