update
This commit is contained in:
1
backend/services/__init__.py
Normal file
1
backend/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services package
|
||||
88
backend/services/email_service.py
Normal file
88
backend/services/email_service.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from jinja2 import Template
|
||||
from config import Config
|
||||
from database import subscribers_collection, articles_collection
|
||||
|
||||
|
||||
def send_newsletter(max_articles=10):
|
||||
"""Send newsletter to all subscribers with AI-summarized articles"""
|
||||
if not Config.EMAIL_USER or not Config.EMAIL_PASSWORD:
|
||||
print("Email credentials not configured")
|
||||
return
|
||||
|
||||
# Get latest articles with AI summaries from database
|
||||
cursor = articles_collection.find(
|
||||
{'summary': {'$exists': True, '$ne': None}}
|
||||
).sort('created_at', -1).limit(max_articles)
|
||||
|
||||
articles = []
|
||||
for doc in cursor:
|
||||
articles.append({
|
||||
'title': doc.get('title', ''),
|
||||
'author': doc.get('author'),
|
||||
'link': doc.get('link', ''),
|
||||
'summary': doc.get('summary', ''),
|
||||
'source': doc.get('source', ''),
|
||||
'published_at': doc.get('published_at', '')
|
||||
})
|
||||
|
||||
if not articles:
|
||||
print("No articles with summaries to send")
|
||||
return
|
||||
|
||||
# Load email template
|
||||
template_path = Path(__file__).parent.parent / 'templates' / 'newsletter_template.html'
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
template_content = f.read()
|
||||
|
||||
template = Template(template_content)
|
||||
|
||||
# Prepare template data
|
||||
now = datetime.now()
|
||||
template_data = {
|
||||
'date': now.strftime('%A, %B %d, %Y'),
|
||||
'year': now.year,
|
||||
'article_count': len(articles),
|
||||
'articles': articles,
|
||||
'unsubscribe_link': 'http://localhost:3000', # Update with actual unsubscribe link
|
||||
'website_link': 'http://localhost:3000'
|
||||
}
|
||||
|
||||
# Render HTML
|
||||
html_content = template.render(**template_data)
|
||||
|
||||
# Get all active subscribers
|
||||
subscribers_cursor = subscribers_collection.find({'status': 'active'})
|
||||
subscribers = [doc['email'] for doc in subscribers_cursor]
|
||||
|
||||
# Send emails
|
||||
for subscriber in subscribers:
|
||||
try:
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = f'Munich News Daily - {datetime.now().strftime("%B %d, %Y")}'
|
||||
msg['From'] = f'Munich News Daily <{Config.EMAIL_USER}>'
|
||||
msg['To'] = subscriber
|
||||
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||
msg['Message-ID'] = f'<{datetime.now().timestamp()}.{subscriber}@dongho.kim>'
|
||||
msg['X-Mailer'] = 'Munich News Daily'
|
||||
|
||||
# Add plain text version as fallback
|
||||
plain_text = "This email requires HTML support. Please view it in an HTML-capable email client."
|
||||
msg.attach(MIMEText(plain_text, 'plain', 'utf-8'))
|
||||
|
||||
# Add HTML version
|
||||
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
|
||||
|
||||
server = smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT)
|
||||
server.starttls()
|
||||
server.login(Config.EMAIL_USER, Config.EMAIL_PASSWORD)
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
print(f"Newsletter sent to {subscriber}")
|
||||
except Exception as e:
|
||||
print(f"Error sending to {subscriber}: {e}")
|
||||
90
backend/services/news_service.py
Normal file
90
backend/services/news_service.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import feedparser
|
||||
from datetime import datetime
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from database import articles_collection, rss_feeds_collection
|
||||
from utils.rss_utils import extract_article_url, extract_article_summary, extract_published_date
|
||||
|
||||
|
||||
def get_active_rss_feeds():
|
||||
"""Get all active RSS feeds from database"""
|
||||
feeds = []
|
||||
cursor = rss_feeds_collection.find({'active': True})
|
||||
for feed in cursor:
|
||||
feeds.append({
|
||||
'name': feed.get('name', ''),
|
||||
'url': feed.get('url', '')
|
||||
})
|
||||
return feeds
|
||||
|
||||
|
||||
def fetch_munich_news():
|
||||
"""Fetch news from Munich news sources"""
|
||||
articles = []
|
||||
|
||||
# Get RSS feeds from database instead of hardcoded list
|
||||
sources = get_active_rss_feeds()
|
||||
|
||||
for source in sources:
|
||||
try:
|
||||
feed = feedparser.parse(source['url'])
|
||||
for entry in feed.entries[:5]: # Get top 5 from each source
|
||||
# Extract article URL using utility function
|
||||
article_url = extract_article_url(entry)
|
||||
|
||||
if not article_url:
|
||||
print(f" ⚠ No valid URL for: {entry.get('title', 'Unknown')[:50]}")
|
||||
continue # Skip entries without valid URL
|
||||
|
||||
# Extract summary
|
||||
summary = extract_article_summary(entry)
|
||||
if summary:
|
||||
summary = summary[:200] + '...' if len(summary) > 200 else summary
|
||||
|
||||
articles.append({
|
||||
'title': entry.get('title', ''),
|
||||
'link': article_url,
|
||||
'summary': summary,
|
||||
'source': source['name'],
|
||||
'published': extract_published_date(entry)
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error fetching from {source['name']}: {e}")
|
||||
|
||||
return articles
|
||||
|
||||
|
||||
def save_articles_to_db(articles):
|
||||
"""Save articles to MongoDB, avoiding duplicates"""
|
||||
saved_count = 0
|
||||
|
||||
for article in articles:
|
||||
try:
|
||||
# Prepare article document
|
||||
article_doc = {
|
||||
'title': article.get('title', ''),
|
||||
'link': article.get('link', ''),
|
||||
'summary': article.get('summary', ''),
|
||||
'source': article.get('source', ''),
|
||||
'published_at': article.get('published', ''),
|
||||
'created_at': datetime.utcnow()
|
||||
}
|
||||
|
||||
# Use update_one with upsert to handle duplicates
|
||||
# This will insert if link doesn't exist, or update if it does
|
||||
result = articles_collection.update_one(
|
||||
{'link': article_doc['link']},
|
||||
{'$setOnInsert': article_doc}, # Only set on insert, don't update existing
|
||||
upsert=True
|
||||
)
|
||||
|
||||
if result.upserted_id:
|
||||
saved_count += 1
|
||||
|
||||
except DuplicateKeyError:
|
||||
# Link already exists, skip
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error saving article {article.get('link', 'unknown')}: {e}")
|
||||
|
||||
if saved_count > 0:
|
||||
print(f"Saved {saved_count} new articles to database")
|
||||
96
backend/services/ollama_service.py
Normal file
96
backend/services/ollama_service.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import requests
|
||||
from config import Config
|
||||
|
||||
|
||||
def list_ollama_models():
|
||||
"""List available models on Ollama server"""
|
||||
if not Config.OLLAMA_ENABLED:
|
||||
return None, "Ollama is not enabled"
|
||||
|
||||
try:
|
||||
url = f"{Config.OLLAMA_BASE_URL}/api/tags"
|
||||
headers = {}
|
||||
if Config.OLLAMA_API_KEY:
|
||||
headers["Authorization"] = f"Bearer {Config.OLLAMA_API_KEY}"
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
models = result.get('models', [])
|
||||
model_names = [model.get('name', '') for model in models]
|
||||
|
||||
return model_names, None
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f"Error listing models: {str(e)}"
|
||||
except Exception as e:
|
||||
return None, f"Unexpected error: {str(e)}"
|
||||
|
||||
|
||||
def call_ollama(prompt, system_prompt=None):
|
||||
"""Call Ollama API to generate text"""
|
||||
if not Config.OLLAMA_ENABLED:
|
||||
return None, "Ollama is not enabled"
|
||||
|
||||
try:
|
||||
url = f"{Config.OLLAMA_BASE_URL}/api/generate"
|
||||
payload = {
|
||||
"model": Config.OLLAMA_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
|
||||
headers = {}
|
||||
if Config.OLLAMA_API_KEY:
|
||||
headers["Authorization"] = f"Bearer {Config.OLLAMA_API_KEY}"
|
||||
|
||||
print(f"Calling Ollama at {url} with model {Config.OLLAMA_MODEL}")
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
response_text = result.get('response', '').strip()
|
||||
|
||||
if not response_text:
|
||||
return None, "Ollama returned empty response"
|
||||
|
||||
return response_text, None
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
error_msg = f"Cannot connect to Ollama server at {Config.OLLAMA_BASE_URL}. Is Ollama running?"
|
||||
print(f"Connection error: {error_msg}")
|
||||
return None, error_msg
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "Request to Ollama timed out after 30 seconds"
|
||||
print(f"Timeout error: {error_msg}")
|
||||
return None, error_msg
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# Check if it's a model not found error
|
||||
if e.response.status_code == 404:
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if 'model' in error_data.get('error', '').lower() and 'not found' in error_data.get('error', '').lower():
|
||||
# Try to get available models
|
||||
available_models, _ = list_ollama_models()
|
||||
if available_models:
|
||||
error_msg = f"Model '{Config.OLLAMA_MODEL}' not found. Available models: {', '.join(available_models)}"
|
||||
else:
|
||||
error_msg = f"Model '{Config.OLLAMA_MODEL}' not found. Use 'ollama list' on the server to see available models."
|
||||
else:
|
||||
error_msg = f"HTTP error from Ollama: {e.response.status_code} - {e.response.text}"
|
||||
except (ValueError, KeyError):
|
||||
error_msg = f"HTTP error from Ollama: {e.response.status_code} - {e.response.text}"
|
||||
else:
|
||||
error_msg = f"HTTP error from Ollama: {e.response.status_code} - {e.response.text}"
|
||||
print(f"HTTP error: {error_msg}")
|
||||
return None, error_msg
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Request error: {str(e)}"
|
||||
print(f"Request error: {error_msg}")
|
||||
return None, error_msg
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
print(f"Unexpected error: {error_msg}")
|
||||
return None, error_msg
|
||||
Reference in New Issue
Block a user