246 lines
7.7 KiB
Python
246 lines
7.7 KiB
Python
"""
|
||
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 <query> - 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"<details{open_attr}>\n<summary>📰 {title}</summary>\n\n{content}\n\n</details>"
|
||
|
||
|
||
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"<li>\n"
|
||
f"<strong>{index}. {title}</strong><br/>\n"
|
||
f"📰 <strong>Source:</strong> {source}{date_str}<br/>\n"
|
||
f"📝 <strong>Summary:</strong> {description}<br/>\n"
|
||
f"🔗 <a href='{url}'>{url}</a>\n"
|
||
f"</li>"
|
||
)
|
||
|
||
|
||
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 = "<ul>\n"
|
||
for i, article in enumerate(articles[:limit], 1):
|
||
content += _format_news_article(article, i) + "\n"
|
||
content += f"</ul>\n\n<em>Fetched {len(articles[:limit])} articles</em>"
|
||
|
||
# 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__ = """
|
||
<details>
|
||
<summary><strong>!news</strong> – Latest news headlines</summary>
|
||
<ul>
|
||
<li><code>!news [top|world|tech|business|science|health|sports|crypto]</code></li>
|
||
<li><code>!news search <query></code></li>
|
||
<li>You can append a number: <code>!news tech 8</code></li>
|
||
</ul>
|
||
<p>Requires <strong>GNEWS_API_KEY</strong> env var.</p>
|
||
</details>
|
||
"""
|