"""
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"
for i, article in enumerate(articles[:limit], 1):
content += _format_news_article(article, i) + "\n"
content += f"
\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
!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.
"""