This commit is contained in:
2025-11-12 22:33:56 +01:00
parent 95669fd211
commit 45df834d5b
12 changed files with 1172 additions and 240 deletions

View File

@@ -224,6 +224,43 @@ def send_newsletter():
}), 500
@admin_bp.route('/api/admin/recent-articles', methods=['GET'])
def get_recent_articles():
"""Get recently summarized articles"""
try:
from database import articles_collection
# Get last 10 articles with summaries, sorted by when they were summarized
articles = list(articles_collection.find(
{'summary': {'$exists': True, '$ne': None}},
{
'title': 1,
'title_en': 1,
'source': 1,
'category': 1,
'summarized_at': 1,
'created_at': 1,
'summary_word_count': 1,
'_id': 0
}
).sort('summarized_at', -1).limit(10))
# Convert datetime to ISO format
for article in articles:
if 'summarized_at' in article and article['summarized_at']:
article['summarized_at'] = article['summarized_at'].isoformat()
if 'created_at' in article and article['created_at']:
article['created_at'] = article['created_at'].isoformat()
return jsonify({
'articles': articles,
'total': len(articles)
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin_bp.route('/api/admin/stats', methods=['GET'])
def get_stats():
"""Get system statistics"""

View File

@@ -1,8 +1,8 @@
"""
RSS Feed management routes
"""
from flask import Blueprint, request, jsonify
from datetime import datetime
from pymongo.errors import DuplicateKeyError
from bson.objectid import ObjectId
import feedparser
from database import rss_feeds_collection
rss_bp = Blueprint('rss', __name__)
@@ -10,145 +10,208 @@ rss_bp = Blueprint('rss', __name__)
@rss_bp.route('/api/rss-feeds', methods=['GET'])
def get_rss_feeds():
"""Get all RSS feeds, optionally filtered by category"""
"""Get all RSS feeds"""
try:
# Get optional category filter
category = request.args.get('category')
feeds = list(rss_feeds_collection.find(
{},
{'_id': 0}
).sort('name', 1))
# Build query
query = {}
if category:
query['category'] = category
# Convert datetime to ISO format
for feed in feeds:
if 'created_at' in feed:
feed['created_at'] = feed['created_at'].isoformat()
return jsonify({
'feeds': feeds,
'total': len(feeds)
}), 200
cursor = rss_feeds_collection.find(query).sort('created_at', -1)
feeds = []
for feed in cursor:
feeds.append({
'id': str(feed['_id']),
'name': feed.get('name', ''),
'url': feed.get('url', ''),
'category': feed.get('category', 'general'),
'active': feed.get('active', True),
'created_at': feed.get('created_at', '').isoformat() if feed.get('created_at') else ''
})
return jsonify({'feeds': feeds}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds', methods=['POST'])
def add_rss_feed():
"""Add a new RSS feed with optional category"""
data = request.json
name = data.get('name', '').strip()
url = data.get('url', '').strip()
category = data.get('category', 'general').strip().lower()
if not name or not url:
return jsonify({'error': 'Name and URL are required'}), 400
if not url.startswith('http://') and not url.startswith('https://'):
return jsonify({'error': 'URL must start with http:// or https://'}), 400
# Validate category (optional, but if provided should be reasonable)
valid_categories = ['general', 'local', 'politics', 'sports', 'culture', 'business', 'technology', 'entertainment']
if category and category not in valid_categories:
# Allow custom categories but warn
pass
"""Add a new RSS feed"""
try:
# Test if the RSS feed is valid
try:
feed = feedparser.parse(url)
if not feed.entries:
return jsonify({'error': 'Invalid RSS feed or no entries found'}), 400
except Exception as e:
return jsonify({'error': f'Failed to parse RSS feed: {str(e)}'}), 400
data = request.get_json()
# Validate required fields
if not data.get('name'):
return jsonify({'error': 'Feed name is required'}), 400
if not data.get('url'):
return jsonify({'error': 'Feed URL is required'}), 400
if not data.get('category'):
return jsonify({'error': 'Category is required'}), 400
# Category validation - accept any string, but recommend common ones
category = data['category'].strip().lower()
if not category:
return jsonify({'error': 'Category cannot be empty'}), 400
data['category'] = category # Normalize to lowercase
# Check if feed already exists
existing = rss_feeds_collection.find_one({'url': data['url']})
if existing:
return jsonify({'error': 'Feed URL already exists'}), 400
# Create feed document
feed_doc = {
'name': name,
'url': url,
'category': category,
'active': True,
'name': data['name'].strip(),
'url': data['url'].strip(),
'category': data['category'],
'active': data.get('active', True),
'created_at': datetime.utcnow()
}
try:
result = rss_feeds_collection.insert_one(feed_doc)
return jsonify({
'message': 'RSS feed added successfully',
'id': str(result.inserted_id),
'category': category
}), 201
except DuplicateKeyError:
return jsonify({'error': 'RSS feed URL already exists'}), 409
rss_feeds_collection.insert_one(feed_doc)
return jsonify({
'message': 'RSS feed added successfully',
'feed': {
'name': feed_doc['name'],
'url': feed_doc['url'],
'category': feed_doc['category'],
'active': feed_doc['active']
}
}), 201
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds/<feed_id>', methods=['DELETE'])
def remove_rss_feed(feed_id):
"""Remove an RSS feed"""
@rss_bp.route('/api/rss-feeds/<feed_name>', methods=['PUT'])
def update_rss_feed(feed_name):
"""Update an RSS feed"""
try:
# Validate ObjectId
try:
obj_id = ObjectId(feed_id)
except Exception:
return jsonify({'error': 'Invalid feed ID'}), 400
data = request.get_json()
result = rss_feeds_collection.delete_one({'_id': obj_id})
# Build update document
update_doc = {}
if 'url' in data:
update_doc['url'] = data['url'].strip()
if 'category' in data:
category = data['category'].strip().lower()
if not category:
return jsonify({'error': 'Category cannot be empty'}), 400
update_doc['category'] = category
if 'active' in data:
update_doc['active'] = bool(data['active'])
if result.deleted_count > 0:
return jsonify({'message': 'RSS feed removed successfully'}), 200
else:
return jsonify({'error': 'RSS feed not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds/<feed_id>/toggle', methods=['PATCH'])
def toggle_rss_feed(feed_id):
"""Toggle RSS feed active status"""
try:
# Validate ObjectId
try:
obj_id = ObjectId(feed_id)
except Exception:
return jsonify({'error': 'Invalid feed ID'}), 400
if not update_doc:
return jsonify({'error': 'No fields to update'}), 400
# Get current status
feed = rss_feeds_collection.find_one({'_id': obj_id})
if not feed:
return jsonify({'error': 'RSS feed not found'}), 404
# Toggle status
new_status = not feed.get('active', True)
result = rss_feeds_collection.update_one(
{'_id': obj_id},
{'$set': {'active': new_status}}
{'name': feed_name},
{'$set': update_doc}
)
if result.modified_count > 0:
return jsonify({
'message': f'RSS feed {"activated" if new_status else "deactivated"} successfully',
'active': new_status
}), 200
if result.matched_count > 0:
return jsonify({'message': 'RSS feed updated successfully'}), 200
else:
return jsonify({'error': 'Failed to update RSS feed'}), 500
return jsonify({'error': 'Feed not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds/categories', methods=['GET'])
def get_categories():
"""Get all unique categories from RSS feeds"""
@rss_bp.route('/api/rss-feeds/<feed_name>', methods=['DELETE'])
def delete_rss_feed(feed_name):
"""Delete an RSS feed"""
try:
categories = rss_feeds_collection.distinct('category')
# Filter out None/empty and sort
categories = sorted([c for c in categories if c])
return jsonify({'categories': categories}), 200
result = rss_feeds_collection.delete_one({'name': feed_name})
if result.deleted_count > 0:
return jsonify({'message': f'RSS feed "{feed_name}" deleted successfully'}), 200
else:
return jsonify({'error': 'Feed not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds/export', methods=['GET'])
def export_rss_feeds():
"""Export all RSS feeds as JSON"""
try:
feeds = list(rss_feeds_collection.find(
{},
{'_id': 0}
).sort('name', 1))
# Convert datetime to ISO format
for feed in feeds:
if 'created_at' in feed:
feed['created_at'] = feed['created_at'].isoformat()
return jsonify({
'feeds': feeds,
'total': len(feeds),
'exported_at': datetime.utcnow().isoformat()
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@rss_bp.route('/api/rss-feeds/import', methods=['POST'])
def import_rss_feeds():
"""Import RSS feeds from JSON"""
try:
data = request.get_json()
if not data or 'feeds' not in data:
return jsonify({'error': 'Invalid import data. Expected {feeds: [...]}'}), 400
feeds = data['feeds']
if not isinstance(feeds, list):
return jsonify({'error': 'feeds must be an array'}), 400
imported = 0
skipped = 0
errors = []
for feed in feeds:
try:
# Validate required fields
if not feed.get('name') or not feed.get('url') or not feed.get('category'):
errors.append(f"Skipped invalid feed: {feed.get('name', 'unknown')}")
skipped += 1
continue
# Check if feed already exists
existing = rss_feeds_collection.find_one({'url': feed['url']})
if existing:
errors.append(f"Skipped duplicate: {feed['name']}")
skipped += 1
continue
# Create feed document
feed_doc = {
'name': feed['name'].strip(),
'url': feed['url'].strip(),
'category': feed['category'].strip().lower(),
'active': feed.get('active', True),
'created_at': datetime.utcnow()
}
rss_feeds_collection.insert_one(feed_doc)
imported += 1
except Exception as e:
errors.append(f"Error importing {feed.get('name', 'unknown')}: {str(e)}")
skipped += 1
return jsonify({
'message': f'Import complete: {imported} imported, {skipped} skipped',
'imported': imported,
'skipped': skipped,
'errors': errors if errors else None
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500

View File

@@ -8,36 +8,69 @@ subscription_bp = Blueprint('subscription', __name__)
@subscription_bp.route('/api/subscribe', methods=['POST'])
def subscribe():
"""Subscribe a user to the newsletter"""
"""Subscribe a user to the newsletter with category preferences"""
data = request.json
email = data.get('email', '').strip().lower()
# Get valid categories from RSS feeds
from database import rss_feeds_collection
valid_categories = rss_feeds_collection.distinct('category')
categories = data.get('categories', valid_categories) # Default: all categories
if not email or '@' not in email:
return jsonify({'error': 'Invalid email address'}), 400
# Validate categories
if not isinstance(categories, list) or not categories:
categories = valid_categories # Default to all if invalid
else:
# Filter to only valid categories (categories that exist in RSS feeds)
categories = [c for c in categories if c in valid_categories]
if not categories:
categories = valid_categories # Default to all if none valid
try:
subscriber_doc = {
'email': email,
'subscribed_at': datetime.utcnow(),
'status': 'active'
'status': 'active',
'categories': categories
}
# Try to insert, if duplicate key error, subscriber already exists
try:
subscribers_collection.insert_one(subscriber_doc)
return jsonify({'message': 'Successfully subscribed!'}), 201
return jsonify({
'message': 'Successfully subscribed!',
'categories': categories
}), 201
except DuplicateKeyError:
# Check if subscriber is active
existing = subscribers_collection.find_one({'email': email})
if existing and existing.get('status') == 'active':
return jsonify({'message': 'Email already subscribed'}), 200
# Update categories even if already subscribed
subscribers_collection.update_one(
{'email': email},
{'$set': {'categories': categories}}
)
return jsonify({
'message': 'Email already subscribed. Preferences updated!',
'categories': categories
}), 200
else:
# Reactivate if previously unsubscribed
subscribers_collection.update_one(
{'email': email},
{'$set': {'status': 'active', 'subscribed_at': datetime.utcnow()}}
{'$set': {
'status': 'active',
'subscribed_at': datetime.utcnow(),
'categories': categories
}}
)
return jsonify({'message': 'Successfully re-subscribed!'}), 200
return jsonify({
'message': 'Successfully re-subscribed!',
'categories': categories
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -95,6 +128,34 @@ def list_subscribers():
return jsonify({'error': str(e)}), 500
@subscription_bp.route('/api/subscribers/<email>', methods=['GET'])
def get_subscriber(email):
"""Get a single subscriber's information"""
try:
email = email.strip().lower()
subscriber = subscribers_collection.find_one(
{'email': email},
{'_id': 0, 'email': 1, 'categories': 1, 'status': 1, 'subscribed_at': 1}
)
if subscriber:
# Convert datetime to ISO format
if 'subscribed_at' in subscriber:
subscriber['subscribed_at'] = subscriber['subscribed_at'].isoformat()
# Ensure categories field exists
if 'categories' not in subscriber:
subscriber['categories'] = ['general', 'local', 'sports']
return jsonify(subscriber), 200
else:
return jsonify({'error': 'Email not found in subscribers'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@subscription_bp.route('/api/subscribers/<email>', methods=['DELETE'])
def remove_subscriber(email):
"""Permanently remove a subscriber from the database"""
@@ -110,3 +171,68 @@ def remove_subscriber(email):
except Exception as e:
return jsonify({'error': str(e)}), 500
@subscription_bp.route('/api/admin/subscribers/delete-all', methods=['DELETE'])
def delete_all_subscribers():
"""Delete all subscribers (admin only)"""
try:
result = subscribers_collection.delete_many({})
return jsonify({
'message': f'Successfully deleted {result.deleted_count} subscribers',
'deleted_count': result.deleted_count
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@subscription_bp.route('/api/categories', methods=['GET'])
def get_categories():
"""Get available newsletter categories from RSS feeds"""
from database import rss_feeds_collection
# Get unique categories from RSS feeds
categories_from_db = rss_feeds_collection.distinct('category')
# Category metadata
category_info = {
'general': {
'name': 'Top Trending',
'description': 'Top trending news and updates',
'icon': '🔥'
},
'local': {
'name': 'Local Events',
'description': 'Local events, culture, and community news',
'icon': '🏛️'
},
'sports': {
'name': 'Sports',
'description': 'Sports news and updates',
'icon': ''
},
'science': {
'name': 'Science & Tech',
'description': 'Science and technology news',
'icon': '🔬'
}
}
# Build category list
categories = []
for cat_id in sorted(categories_from_db):
info = category_info.get(cat_id, {
'name': cat_id.title(),
'description': f'{cat_id.title()} news',
'icon': '📄'
})
categories.append({
'id': cat_id,
'name': info['name'],
'description': info['description'],
'icon': info['icon']
})
return jsonify({'categories': categories}), 200