Files
FunguyBot/plugins/news.py
T

246 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 &lt;query&gt;</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>
"""