""" News Aggregator Plugin for Funguy Bot Fetches latest headlines from various news categories using GNews API. Free tier: 100 requests/day """ import logging import aiohttp import os import simplematrixbotlib as botlib from plugins.common import html_escape, collapsible_summary # API key loaded centrally GNEWS_API_KEY = os.getenv("GNEWS_API_KEY") DEFAULT_ARTICLES = 5 MAX_ARTICLES = 10 CATEGORIES = { "top": "general", "world": "world", "tech": "technology", "business": "business", "entertainment": "entertainment", "science": "science", "health": "health", "sports": "sports", "crypto": "cryptocurrency" } def _format_news_article(article, index): """Format a single news article as an HTML list item.""" title = html_escape(article.get("title", "No title")) source = html_escape((article.get("source") or {}).get("name", "Unknown")) url = article.get("url", "#") description = html_escape(article.get("description", "No description available")) if len(description) > 300: description = description[:297] + "..." published = article.get("publishedAt", "") 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="general", query=None, limit=DEFAULT_ARTICLES): 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: url = f"{base_url}/search" params = { "q": query, "apikey": GNEWS_API_KEY, "lang": "en", "max": limit, "country": "us" } else: 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 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") 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: safe_title = html_escape(query) await bot.api.send_text_message(room.room_id, f"🔍 Searching for: *{safe_title}*...") articles = await _fetch_news(query=query, limit=limit) title = f"Search Results: '{safe_title}'" 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 = collapsible_summary(title, content) 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

    Requires GNEWS_API_KEY env var.

    """