""" News Aggregator Plugin for Funguy Bot Fetches latest headlines from various news categories using GNews API. Free tier: 100 requests/day Commands: !news - Get top headlines (default) !news top - Top headlines !news world - World news !news tech - Technology news !news business - Business news !news science - Science news !news health - Health news !news crypto - Cryptocurrency news !news search - Search for specific news """ import logging import aiohttp import os from typing import Optional, Dict, Any, List from dotenv import load_dotenv # Load environment variables load_dotenv() # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # Get API key from environment variable GNEWS_API_KEY = os.getenv("GNEWS_API_KEY") # Number of articles to return per command DEFAULT_ARTICLES = 5 MAX_ARTICLES = 10 # Category mapping CATEGORIES = { "top": "general", "world": "world", "tech": "technology", "business": "business", "entertainment": "entertainment", "science": "science", "health": "health", "sports": "sports", "crypto": "cryptocurrency" } # --------------------------------------------------------------------------- # Helper Functions # --------------------------------------------------------------------------- def _format_collapsible(title: str, content: str, expanded: bool = False) -> str: """Format content in a collapsible details/summary block.""" open_attr = ' open' if expanded else '' return f"\n📰 {title}\n\n{content}\n\n" def _format_news_article(article: Dict, index: int) -> str: """Format a single news article as an HTML list item.""" title = article.get("title", "No title") source = article.get("source", {}).get("name", "Unknown source") url = article.get("url", "#") description = article.get("description", "No description available") published = article.get("publishedAt", "") # Truncate description if too long if len(description) > 300: description = description[:297] + "..." # Format date if available date_str = "" if published: try: from datetime import datetime dt = datetime.fromisoformat(published.replace('Z', '+00:00')) date_str = f" | 📅 {dt.strftime('%Y-%m-%d %H:%M')}" except: pass return ( f"
  • \n" f"{index}. {title}
    \n" f"📰 Source: {source}{date_str}
    \n" f"📝 Summary: {description}
    \n" f"🔗 {url}\n" f"
  • " ) async def _fetch_news(category: str = "general", query: str = None, limit: int = DEFAULT_ARTICLES) -> Optional[List[Dict]]: """Fetch news articles from GNews API.""" if not GNEWS_API_KEY: logging.error("GNews API key not configured. Set GNEWS_API_KEY in .env file") return None base_url = "https://gnews.io/api/v4" if query: # Search endpoint url = f"{base_url}/search" params = { "q": query, "apikey": GNEWS_API_KEY, "lang": "en", "max": limit, "country": "us" } else: # Top headlines endpoint url = f"{base_url}/top-headlines" params = { "apikey": GNEWS_API_KEY, "lang": "en", "country": "us", "max": limit } if category and category != "general": params["category"] = category try: async with aiohttp.ClientSession() as session: async with session.get(url, params=params) as response: if response.status == 200: data = await response.json() return data.get("articles", []) else: logging.error(f"GNews API error: {response.status}") return None except Exception as e: logging.error(f"Error fetching news: {e}") return None # --------------------------------------------------------------------------- # Plugin Setup # --------------------------------------------------------------------------- def setup(bot): """Initialize plugin with bot instance.""" global GNEWS_API_KEY GNEWS_API_KEY = os.getenv("GNEWS_API_KEY") if GNEWS_API_KEY: logging.info("News plugin loaded with API key") else: logging.warning("News plugin loaded but GNEWS_API_KEY not set in .env file") # --------------------------------------------------------------------------- # Command Handler # --------------------------------------------------------------------------- async def handle_command(room, message, bot, prefix, config): """Handle !news commands.""" import simplematrixbotlib as botlib match = botlib.MessageMatch(room, message, bot, prefix) if not (match.is_not_from_this_bot() and match.prefix() and match.command("news")): return global GNEWS_API_KEY if not GNEWS_API_KEY: GNEWS_API_KEY = os.getenv("GNEWS_API_KEY") if not GNEWS_API_KEY: await bot.api.send_text_message( room.room_id, "⚠️ News plugin is not configured. Please set GNEWS_API_KEY in .env file and restart the bot." ) return args = match.args() # Parse command arguments category = "top" query = None limit = DEFAULT_ARTICLES if args: command = args[0].lower() if len(args) >= 2 and args[-1].isdigit(): limit = min(int(args[-1]), MAX_ARTICLES) args = args[:-1] if args: command = args[0].lower() if command == "search" and len(args) >= 2: query = " ".join(args[1:]) category = None elif command in CATEGORIES: category = CATEGORIES[command] else: query = " ".join(args) category = None # Fetch news if query: await bot.api.send_text_message(room.room_id, f"🔍 Searching for: *{query}*...") articles = await _fetch_news(query=query, limit=limit) title = f"Search Results: '{query}'" else: articles = await _fetch_news(category=category, limit=limit) category_name = next((k for k, v in CATEGORIES.items() if v == category), category) title = f"Top {category_name.title()} News" if not articles: await bot.api.send_text_message(room.room_id, "❌ Failed to fetch news or no results found.") return # Build content as HTML list content = "\n\nFetched {len(articles[:limit])} articles" # Format as collapsible and send response = _format_collapsible(title, content, expanded=False) await bot.api.send_markdown_message(room.room_id, response) logging.info(f"Sent news to {room.room_id}: category={category}, query={query}") # --------------------------------------------------------------------------- # Plugin Metadata # --------------------------------------------------------------------------- __version__ = "1.0.0" __author__ = "Funguy Bot" __description__ = "News headlines via GNews API" __help__ = """
    !news – Latest news headlines
    • !news [top|world|tech|business|science|health|sports|crypto]
    • !news search <query>
    • You can append a number: !news tech 8

    Requires GNEWS_API_KEY env var.

    """