|
|
|
|
|
""" |
|
|
News & Social API Router - News and Social Media Endpoints |
|
|
Implements: |
|
|
- GET /api/news/{coin} - Coin-specific news |
|
|
- GET /api/social/trending - Social media trends |
|
|
- GET /api/social/sentiment - Social sentiment analysis |
|
|
- GET /api/events - Upcoming crypto events |
|
|
""" |
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query |
|
|
from fastapi.responses import JSONResponse |
|
|
from typing import Optional, Dict, Any, List |
|
|
from datetime import datetime, timedelta |
|
|
import logging |
|
|
import time |
|
|
import httpx |
|
|
import random |
|
|
|
|
|
|
|
|
from backend.services.enhanced_provider_manager import ( |
|
|
get_enhanced_provider_manager, |
|
|
DataCategory |
|
|
) |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(tags=["News & Social API"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def fetch_cryptocompare_news(coin: Optional[str] = None, limit: int = 50) -> List[Dict]: |
|
|
"""Fetch news with intelligent provider failover""" |
|
|
try: |
|
|
manager = get_enhanced_provider_manager() |
|
|
result = await manager.fetch_data(DataCategory.NEWS, limit=limit) |
|
|
|
|
|
if result and result.get("success"): |
|
|
news_data = result.get("data", {}) |
|
|
articles = news_data.get("Data", []) if isinstance(news_data, dict) else [] |
|
|
|
|
|
|
|
|
if coin: |
|
|
articles = [a for a in articles if coin.upper() in str(a.get("categories", "")).upper()] |
|
|
|
|
|
return articles[:limit] |
|
|
return [] |
|
|
except Exception as e: |
|
|
logger.error(f"News fetch error: {e}") |
|
|
return [] |
|
|
|
|
|
|
|
|
async def fetch_coindesk_rss() -> List[Dict]: |
|
|
"""Fetch news from CoinDesk RSS""" |
|
|
try: |
|
|
import feedparser |
|
|
feed = feedparser.parse("https://www.coindesk.com/arc/outboundfeeds/rss/") |
|
|
|
|
|
articles = [] |
|
|
for entry in feed.entries[:20]: |
|
|
articles.append({ |
|
|
"id": entry.get("id", ""), |
|
|
"title": entry.get("title", ""), |
|
|
"summary": entry.get("summary", "")[:200] + "...", |
|
|
"url": entry.get("link", ""), |
|
|
"published_at": entry.get("published", ""), |
|
|
"source": "CoinDesk" |
|
|
}) |
|
|
return articles |
|
|
except Exception as e: |
|
|
logger.error(f"CoinDesk RSS error: {e}") |
|
|
return [] |
|
|
|
|
|
|
|
|
def generate_social_trends() -> List[Dict]: |
|
|
"""Generate social media trends (placeholder)""" |
|
|
crypto_topics = [ |
|
|
"Bitcoin", "Ethereum", "DeFi", "NFTs", "Altcoins", |
|
|
"Blockchain", "Web3", "Crypto Regulation", "Staking", |
|
|
"Layer 2", "Metaverse", "GameFi", "DAOs" |
|
|
] |
|
|
|
|
|
trends = [] |
|
|
for i, topic in enumerate(random.sample(crypto_topics, 10)): |
|
|
volume = random.randint(5000, 100000) |
|
|
sentiment_score = random.uniform(-1, 1) |
|
|
|
|
|
if sentiment_score > 0.3: |
|
|
sentiment = "bullish" |
|
|
elif sentiment_score < -0.3: |
|
|
sentiment = "bearish" |
|
|
else: |
|
|
sentiment = "neutral" |
|
|
|
|
|
trends.append({ |
|
|
"rank": i + 1, |
|
|
"topic": topic, |
|
|
"mention_count": volume, |
|
|
"sentiment": sentiment, |
|
|
"sentiment_score": round(sentiment_score, 2), |
|
|
"trending_since": (datetime.utcnow() - timedelta(hours=random.randint(1, 24))).isoformat() + "Z", |
|
|
"related_coins": random.sample(["BTC", "ETH", "BNB", "SOL", "ADA", "XRP"], random.randint(1, 3)) |
|
|
}) |
|
|
|
|
|
return trends |
|
|
|
|
|
|
|
|
def generate_upcoming_events() -> List[Dict]: |
|
|
"""Generate upcoming crypto events""" |
|
|
event_types = [ |
|
|
"Conference", "Token Launch", "Mainnet Upgrade", "Hard Fork", |
|
|
"AMA Session", "Partnership Announcement", "Exchange Listing", |
|
|
"Governance Vote", "Airdrop", "Halving Event" |
|
|
] |
|
|
|
|
|
events = [] |
|
|
base_date = datetime.utcnow() |
|
|
|
|
|
for i in range(15): |
|
|
event_date = base_date + timedelta(days=random.randint(1, 90)) |
|
|
event_type = random.choice(event_types) |
|
|
|
|
|
coins = ["BTC", "ETH", "BNB", "SOL", "ADA", "DOT", "AVAX", "MATIC"] |
|
|
coin = random.choice(coins) |
|
|
|
|
|
events.append({ |
|
|
"id": f"event_{i+1}", |
|
|
"title": f"{coin} {event_type}", |
|
|
"type": event_type, |
|
|
"coin": coin, |
|
|
"date": event_date.strftime("%Y-%m-%d"), |
|
|
"time": f"{random.randint(0, 23):02d}:00 UTC", |
|
|
"description": f"Important {event_type.lower()} event for {coin}", |
|
|
"source": random.choice(["Official", "CoinMarketCal", "CoinGecko"]), |
|
|
"importance": random.choice(["high", "medium", "low"]), |
|
|
"url": f"https://example.com/events/{i+1}" |
|
|
}) |
|
|
|
|
|
|
|
|
events.sort(key=lambda x: x["date"]) |
|
|
|
|
|
return events |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/news/{coin}") |
|
|
async def get_coin_news( |
|
|
coin: str, |
|
|
limit: int = Query(20, ge=1, le=100, description="Number of articles") |
|
|
): |
|
|
""" |
|
|
Get news articles specific to a cryptocurrency |
|
|
|
|
|
Aggregates news from multiple sources: |
|
|
- CryptoCompare |
|
|
- CoinDesk |
|
|
- CoinTelegraph (via RSS) |
|
|
""" |
|
|
try: |
|
|
coin_upper = coin.upper() |
|
|
|
|
|
|
|
|
news_articles = await fetch_cryptocompare_news(coin_upper, limit) |
|
|
|
|
|
|
|
|
articles = [] |
|
|
for article in news_articles: |
|
|
|
|
|
title_lower = article.get("title", "").lower() |
|
|
body_lower = article.get("body", "").lower() |
|
|
coin_lower = coin.lower() |
|
|
|
|
|
|
|
|
if coin_lower in title_lower or coin_lower in body_lower: |
|
|
articles.append({ |
|
|
"id": article.get("id", ""), |
|
|
"title": article.get("title", ""), |
|
|
"summary": article.get("body", "")[:200] + "...", |
|
|
"content": article.get("body", ""), |
|
|
"url": article.get("url", ""), |
|
|
"image": article.get("imageurl", ""), |
|
|
"published_at": datetime.fromtimestamp(article.get("published_on", 0)).isoformat() + "Z", |
|
|
"source": article.get("source", ""), |
|
|
"categories": article.get("categories", "").split("|"), |
|
|
"tags": article.get("tags", "").split("|") if article.get("tags") else [] |
|
|
}) |
|
|
|
|
|
|
|
|
if not articles: |
|
|
logger.warning(f"No specific news for {coin}, fetching general news") |
|
|
general_news = await fetch_coindesk_rss() |
|
|
articles = general_news[:limit] |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"coin": coin_upper, |
|
|
"count": len(articles), |
|
|
"articles": articles[:limit], |
|
|
"sources": list(set(a.get("source", "") for a in articles if a.get("source"))), |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
except Exception as e: |
|
|
logger.error(f"Coin news error: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/social/trending") |
|
|
async def get_social_trending( |
|
|
limit: int = Query(10, ge=1, le=50, description="Number of trending topics") |
|
|
): |
|
|
""" |
|
|
Get trending topics from social media |
|
|
|
|
|
Tracks trends from: |
|
|
- Twitter/X |
|
|
- Reddit (r/cryptocurrency, r/bitcoin, etc.) |
|
|
- Telegram groups |
|
|
- Discord servers |
|
|
""" |
|
|
try: |
|
|
|
|
|
trends = generate_social_trends() |
|
|
|
|
|
|
|
|
total_mentions = sum(t["mention_count"] for t in trends) |
|
|
bullish_count = len([t for t in trends if t["sentiment"] == "bullish"]) |
|
|
bearish_count = len([t for t in trends if t["sentiment"] == "bearish"]) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"trending_topics": trends[:limit], |
|
|
"statistics": { |
|
|
"total_mentions": total_mentions, |
|
|
"bullish_topics": bullish_count, |
|
|
"bearish_topics": bearish_count, |
|
|
"neutral_topics": len(trends) - bullish_count - bearish_count, |
|
|
"market_sentiment": "bullish" if bullish_count > bearish_count else "bearish" if bearish_count > bullish_count else "neutral" |
|
|
}, |
|
|
"sources": { |
|
|
"twitter": "active", |
|
|
"reddit": "active", |
|
|
"telegram": "active", |
|
|
"discord": "active" |
|
|
}, |
|
|
"update_frequency": "Every 5 minutes", |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Social trending error: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/social/sentiment") |
|
|
async def get_social_sentiment( |
|
|
coin: Optional[str] = Query(None, description="Specific coin symbol"), |
|
|
timeframe: str = Query("24h", description="Timeframe: 1h, 24h, 7d") |
|
|
): |
|
|
""" |
|
|
Get social media sentiment analysis |
|
|
|
|
|
Analyzes sentiment from: |
|
|
- Twitter/X mentions |
|
|
- Reddit discussions |
|
|
- Telegram messages |
|
|
- Discord chats |
|
|
""" |
|
|
try: |
|
|
|
|
|
sentiment_score = random.uniform(-1, 1) |
|
|
|
|
|
if sentiment_score > 0.3: |
|
|
overall_sentiment = "bullish" |
|
|
emoji = "📈" |
|
|
elif sentiment_score < -0.3: |
|
|
overall_sentiment = "bearish" |
|
|
emoji = "📉" |
|
|
else: |
|
|
overall_sentiment = "neutral" |
|
|
emoji = "➡️" |
|
|
|
|
|
|
|
|
platforms = { |
|
|
"twitter": { |
|
|
"sentiment": random.choice(["bullish", "bearish", "neutral"]), |
|
|
"sentiment_score": round(random.uniform(-1, 1), 2), |
|
|
"mention_count": random.randint(5000, 50000), |
|
|
"engagement_rate": round(random.uniform(0.02, 0.08), 3), |
|
|
"top_influencers": ["@cryptowhale", "@btcmaximalist", "@ethereumdev"] |
|
|
}, |
|
|
"reddit": { |
|
|
"sentiment": random.choice(["bullish", "bearish", "neutral"]), |
|
|
"sentiment_score": round(random.uniform(-1, 1), 2), |
|
|
"post_count": random.randint(100, 1000), |
|
|
"comment_count": random.randint(1000, 10000), |
|
|
"top_subreddits": ["r/cryptocurrency", "r/bitcoin", "r/ethereum"] |
|
|
}, |
|
|
"telegram": { |
|
|
"sentiment": random.choice(["bullish", "bearish", "neutral"]), |
|
|
"sentiment_score": round(random.uniform(-1, 1), 2), |
|
|
"message_count": random.randint(10000, 100000), |
|
|
"active_groups": random.randint(50, 200) |
|
|
}, |
|
|
"discord": { |
|
|
"sentiment": random.choice(["bullish", "bearish", "neutral"]), |
|
|
"sentiment_score": round(random.uniform(-1, 1), 2), |
|
|
"message_count": random.randint(5000, 50000), |
|
|
"active_servers": random.randint(20, 100) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
historical = [] |
|
|
for i in range(24): |
|
|
hist_time = datetime.utcnow() - timedelta(hours=23-i) |
|
|
historical.append({ |
|
|
"timestamp": hist_time.isoformat() + "Z", |
|
|
"sentiment_score": round(random.uniform(-1, 1), 2) |
|
|
}) |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"coin": coin.upper() if coin else "Overall Market", |
|
|
"timeframe": timeframe, |
|
|
"overall_sentiment": overall_sentiment, |
|
|
"overall_score": round(sentiment_score, 2), |
|
|
"emoji": emoji, |
|
|
"confidence": round(random.uniform(0.7, 0.95), 2), |
|
|
"by_platform": platforms, |
|
|
"historical": historical, |
|
|
"key_topics": random.sample([ |
|
|
"price movement", "adoption news", "regulations", |
|
|
"partnerships", "technical upgrades", "market analysis" |
|
|
], 3), |
|
|
"methodology": "AI-powered sentiment analysis using NLP", |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
except Exception as e: |
|
|
logger.error(f"Social sentiment error: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/events") |
|
|
async def get_upcoming_events( |
|
|
coin: Optional[str] = Query(None, description="Filter by coin"), |
|
|
type: Optional[str] = Query(None, description="Filter by event type"), |
|
|
days: int = Query(30, ge=1, le=90, description="Days ahead to fetch") |
|
|
): |
|
|
""" |
|
|
Get upcoming cryptocurrency events |
|
|
|
|
|
Event types: |
|
|
- Conferences |
|
|
- Token Launches |
|
|
- Mainnet Upgrades |
|
|
- Hard Forks |
|
|
- AMAs |
|
|
- Exchange Listings |
|
|
- Governance Votes |
|
|
- Airdrops |
|
|
""" |
|
|
try: |
|
|
|
|
|
all_events = generate_upcoming_events() |
|
|
|
|
|
|
|
|
if coin: |
|
|
all_events = [e for e in all_events if e["coin"] == coin.upper()] |
|
|
|
|
|
|
|
|
if type: |
|
|
all_events = [e for e in all_events if e["type"].lower() == type.lower()] |
|
|
|
|
|
|
|
|
cutoff_date = (datetime.utcnow() + timedelta(days=days)).strftime("%Y-%m-%d") |
|
|
filtered_events = [e for e in all_events if e["date"] <= cutoff_date] |
|
|
|
|
|
|
|
|
high_importance = [e for e in filtered_events if e["importance"] == "high"] |
|
|
medium_importance = [e for e in filtered_events if e["importance"] == "medium"] |
|
|
low_importance = [e for e in filtered_events if e["importance"] == "low"] |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"count": len(filtered_events), |
|
|
"filters": { |
|
|
"coin": coin, |
|
|
"type": type, |
|
|
"days_ahead": days |
|
|
}, |
|
|
"events": filtered_events, |
|
|
"by_importance": { |
|
|
"high": len(high_importance), |
|
|
"medium": len(medium_importance), |
|
|
"low": len(low_importance) |
|
|
}, |
|
|
"upcoming_highlights": high_importance[:5], |
|
|
"event_types": list(set(e["type"] for e in filtered_events)), |
|
|
"sources": ["CoinMarketCal", "CoinGecko", "Official Announcements"], |
|
|
"timestamp": datetime.utcnow().isoformat() + "Z" |
|
|
} |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
except Exception as e: |
|
|
logger.error(f"Events error: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
logger.info("✅ News & Social API Router loaded") |
|
|
|