206 lines
8.7 KiB
Python
206 lines
8.7 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
from database import get_db, DiscountCode, ReleaseParticipant
|
|
import datetime
|
|
import logging
|
|
from gmail_service import GmailService
|
|
import re
|
|
import time
|
|
import asyncio
|
|
from typing import List, Dict, Optional
|
|
|
|
router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
|
|
logger = logging.getLogger("publish-request")
|
|
|
|
# Official Project Signature
|
|
SIGNATURE_HTML = """
|
|
<br><br>
|
|
<span style="color: #888;">--</span><br>
|
|
<div dir="ltr">
|
|
<table border="0" cellspacing="0" cellpadding="0" style="border-collapse:collapse; margin-top: 5px;">
|
|
<tbody>
|
|
<tr>
|
|
<td width="220" valign="top" style="padding-right: 15px;">
|
|
<img width="200" src="https://lh3.googleusercontent.com/d/1K7RODOqKE2e1nRJ3D4dEWdjthoTMyXUq" alt="Kinderfotos Erding Logo" style="display: block;">
|
|
</td>
|
|
<td valign="bottom" style="padding-left: 15px; border-left: 1px solid #ddd; font-family: sans-serif; font-size: 13px; color: #333; line-height: 1.5;">
|
|
<p style="margin: 0;"><b>Kinderfotos Erding</b> | <a href="http://www.kinderfotos-erding.de/" target="_blank" style="color: #1155cc; text-decoration: none;">www.kinderfotos-erding.de</a></p>
|
|
<p style="margin: 0; color: #666;">Gartenstr. 10 | 85445 Oberding | 08122-8470867</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
"""
|
|
|
|
class CodesUpload(BaseModel):
|
|
codes: str # comma separated
|
|
|
|
class SendReleaseRequest(BaseModel):
|
|
emails: List[Dict[str, str]]
|
|
scheduled_time: Optional[str] = None # e.g. "10:00"
|
|
participants: Optional[List[Dict[str, str]]] = None # [{email, first_name}]
|
|
|
|
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db_session_factory):
|
|
try:
|
|
# Calculate delay
|
|
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) # Berlin Time Approx
|
|
target_h, target_m = map(int, scheduled_time.split(":"))
|
|
target_time = now.replace(hour=target_h, minute=target_m, second=0, microsecond=0)
|
|
|
|
if target_time < now:
|
|
target_time += datetime.timedelta(days=1)
|
|
|
|
delay_seconds = (target_time - now).total_seconds()
|
|
logger.info(f"Scheduling {len(emails)} emails for {scheduled_time} (in {delay_seconds} seconds)")
|
|
|
|
await asyncio.sleep(delay_seconds)
|
|
|
|
# We need a fresh DB session for the background task
|
|
db = db_session_factory()
|
|
try:
|
|
service = GmailService(db)
|
|
success_count = 0
|
|
for email_data in emails:
|
|
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
|
|
success_count += 1
|
|
await asyncio.sleep(1) # Rate limiting
|
|
|
|
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
|
|
finally:
|
|
db.close()
|
|
except Exception as e:
|
|
logger.exception("Error in delayed_send background task")
|
|
|
|
@router.post("/send")
|
|
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
|
# Store participant names for later (webhook)
|
|
if data.participants:
|
|
for p in data.participants:
|
|
email = p.get("email", "").strip().lower()
|
|
first_name = p.get("first_name", "").strip()
|
|
if email and first_name:
|
|
existing = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
|
|
if existing:
|
|
existing.first_name = first_name
|
|
else:
|
|
db.add(ReleaseParticipant(email=email, first_name=first_name))
|
|
db.commit()
|
|
|
|
if data.scheduled_time:
|
|
# Pass a way to get a new session to the background task
|
|
from database import SessionLocal
|
|
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, SessionLocal)
|
|
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
|
|
|
|
# Immediate send
|
|
service = GmailService(db)
|
|
success = 0
|
|
failed = []
|
|
for email_data in data.emails:
|
|
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
|
|
success += 1
|
|
else:
|
|
failed.append(email_data["to"])
|
|
|
|
return {"status": "success", "success": success, "failed": failed}
|
|
|
|
@router.get("/stats")
|
|
def get_stats(db: Session = Depends(get_db)):
|
|
total = db.query(DiscountCode).count()
|
|
used = db.query(DiscountCode).filter(DiscountCode.is_used == 1).count()
|
|
available = total - used
|
|
return {"total": total, "used": used, "available": available}
|
|
|
|
@router.post("/codes")
|
|
def upload_codes(data: CodesUpload, db: Session = Depends(get_db)):
|
|
codes_list = [c.strip() for c in data.codes.split(",") if c.strip()]
|
|
added = 0
|
|
for code in set(codes_list):
|
|
existing = db.query(DiscountCode).filter(DiscountCode.code == code).first()
|
|
if not existing:
|
|
new_code = DiscountCode(code=code, is_used=0)
|
|
db.add(new_code)
|
|
added += 1
|
|
db.commit()
|
|
return {"status": "success", "added": added}
|
|
|
|
class WebhookData(BaseModel):
|
|
email: str
|
|
|
|
@router.post("/webhook")
|
|
async def handle_webhook(request: Request, db: Session = Depends(get_db)):
|
|
# Try to parse JSON from Google Forms webhook
|
|
try:
|
|
data = await request.json()
|
|
except:
|
|
raise HTTPException(status_code=400, detail="Invalid JSON")
|
|
|
|
# We expect {"email": "..."} or similar from the Google Apps Script
|
|
email = data.get("email") or data.get("Email")
|
|
if not email:
|
|
logger.error(f"Webhook received without email: {data}")
|
|
return {"status": "error", "message": "Email not found in webhook payload"}
|
|
|
|
email = email.strip().lower()
|
|
|
|
# Check if this email already got a code
|
|
already_assigned = db.query(DiscountCode).filter(DiscountCode.assigned_to_email == email).first()
|
|
if already_assigned:
|
|
logger.info(f"Email {email} already received code {already_assigned.code}")
|
|
return {"status": "success", "message": "Already sent"}
|
|
|
|
# Get a free code
|
|
free_code = db.query(DiscountCode).filter(DiscountCode.is_used == 0).first()
|
|
if not free_code:
|
|
logger.error("NO FREE DISCOUNT CODES LEFT!")
|
|
return {"status": "error", "message": "No codes available"}
|
|
|
|
# Look up participant name
|
|
participant = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
|
|
first_name = participant.first_name if participant else "Ihr Lieben"
|
|
|
|
# Mark as used
|
|
free_code.is_used = 1
|
|
free_code.assigned_to_email = email
|
|
free_code.used_at = datetime.datetime.utcnow()
|
|
db.commit()
|
|
|
|
# Send Thank You Email with GmailService
|
|
service = GmailService(db)
|
|
subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
|
|
|
|
# Image provided by user
|
|
INSTRUCTIONS_IMAGE_URL = "https://mail.google.com/mail/u/2?ui=2&ik=719adaa3c5&attid=0.1&permmsgid=msg-a:r7482671925923393616&th=196e322c399dbc7f&view=fimg&fur=ip&permmsgid=msg-a:r7482671925923393616&sz=s0-l75-ft&attbid=ANGjdJ9_U6ayMFgwbupt4HalTKO867IHx6N70eNbPfQmTLNzRXilJxI-n8a1gjM8xVcP5HEOgaVxfp3FnJPzTYEEYhK4gSU-Il_0a6OtzFYscp55_W4iyxuxjyPvK4&disp=emb&realattid=ii_maspzxv50&zw"
|
|
|
|
body_html = f"""
|
|
<p>Hallo {first_name},</p>
|
|
<p>Vielen Dank nochmal für die Freigabe zur Veröffentlichung, das ist super nett von Euch!</p>
|
|
<p>Hier ist euer Gutscheincode über 25 Euro: <strong style="font-size: 18px; color: #4F46E5;">{free_code.code}</strong></p>
|
|
<p>Um den Gutschein einzugeben, musst du auf den Preis des Warenkorbs drücken (über dem Button zur Kasse gehen):</p>
|
|
<p><img src="{INSTRUCTIONS_IMAGE_URL}" alt="Anleitung Gutschein einlösen" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"></p>
|
|
<p>Liebe Grüße,<br>das Team von Kinderfotos Erding</p>
|
|
{SIGNATURE_HTML}
|
|
"""
|
|
|
|
try:
|
|
success = service.send_email(email, subject, body_html)
|
|
if success:
|
|
logger.info(f"Successfully sent code {free_code.code} to {email}")
|
|
return {"status": "success", "message": "Email sent"}
|
|
else:
|
|
logger.error(f"Failed to send email to {email}")
|
|
free_code.is_used = 0
|
|
free_code.assigned_to_email = None
|
|
free_code.used_at = None
|
|
db.commit()
|
|
return {"status": "error", "message": "Failed to send email"}
|
|
except Exception as e:
|
|
logger.exception("Error sending webhook email")
|
|
free_code.is_used = 0
|
|
free_code.assigned_to_email = None
|
|
free_code.used_at = None
|
|
db.commit()
|
|
return {"status": "error", "message": str(e)}
|