refactor: async I/O, input sanitisation, and shared utilities cleanup
This commit is contained in:
+13
-66
@@ -3,40 +3,19 @@ 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
|
||||
import simplematrixbotlib as botlib
|
||||
from plugins.common import html_escape, collapsible_summary
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Get API key from environment variable
|
||||
# API key loaded centrally
|
||||
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",
|
||||
@@ -49,30 +28,15 @@ CATEGORIES = {
|
||||
"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:
|
||||
def _format_news_article(article, index):
|
||||
"""Format a single news article as an HTML list item."""
|
||||
title = article.get("title", "No title")
|
||||
source = article.get("source", {}).get("name", "Unknown source")
|
||||
title = html_escape(article.get("title", "No title"))
|
||||
source = html_escape((article.get("source") or {}).get("name", "Unknown"))
|
||||
url = article.get("url", "#")
|
||||
description = article.get("description", "No description available")
|
||||
published = article.get("publishedAt", "")
|
||||
|
||||
# Truncate description if too long
|
||||
description = html_escape(article.get("description", "No description available"))
|
||||
if len(description) > 300:
|
||||
description = description[:297] + "..."
|
||||
|
||||
# Format date if available
|
||||
published = article.get("publishedAt", "")
|
||||
date_str = ""
|
||||
if published:
|
||||
try:
|
||||
@@ -81,7 +45,6 @@ def _format_news_article(article: Dict, index: int) -> str:
|
||||
date_str = f" | 📅 {dt.strftime('%Y-%m-%d %H:%M')}"
|
||||
except:
|
||||
pass
|
||||
|
||||
return (
|
||||
f"<li>\n"
|
||||
f"<strong>{index}. {title}</strong><br/>\n"
|
||||
@@ -91,17 +54,13 @@ def _format_news_article(article: Dict, index: int) -> str:
|
||||
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."""
|
||||
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:
|
||||
# Search endpoint
|
||||
url = f"{base_url}/search"
|
||||
params = {
|
||||
"q": query,
|
||||
@@ -111,7 +70,6 @@ async def _fetch_news(category: str = "general", query: str = None, limit: int =
|
||||
"country": "us"
|
||||
}
|
||||
else:
|
||||
# Top headlines endpoint
|
||||
url = f"{base_url}/top-headlines"
|
||||
params = {
|
||||
"apikey": GNEWS_API_KEY,
|
||||
@@ -135,26 +93,15 @@ async def _fetch_news(category: str = "general", query: str = None, limit: int =
|
||||
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
|
||||
@@ -201,9 +148,10 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
|
||||
# Fetch news
|
||||
if query:
|
||||
await bot.api.send_text_message(room.room_id, f"🔍 Searching for: *{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: '{query}'"
|
||||
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)
|
||||
@@ -220,11 +168,10 @@ async def handle_command(room, message, bot, prefix, config):
|
||||
content += f"</ul>\n\n<em>Fetched {len(articles[:limit])} articles</em>"
|
||||
|
||||
# Format as collapsible and send
|
||||
response = _format_collapsible(title, content, expanded=False)
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user