feat: add email, scheduler, and Zulip integration services

- Add email service for sending meeting invites with ICS attachments
- Add scheduler for background calendar sync jobs
- Add Zulip service for meeting notifications
- Make ics_url optional for participants
- Add /api/schedule endpoint with 2-hour lead time validation
- Update frontend to support scheduling flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Joyce
2026-01-21 13:52:02 -05:00
parent d585cf8613
commit 26311c867a
17 changed files with 403 additions and 59 deletions

View File

@@ -16,12 +16,24 @@ from app.schemas import (
ParticipantCreate,
ParticipantResponse,
SyncResponse,
ScheduleRequest,
)
from app.scheduler import start_scheduler, stop_scheduler
from app.email_service import send_meeting_invite
from app.zulip_service import send_zulip_notification
from contextlib import asynccontextmanager
from datetime import datetime, timezone, timedelta
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Common Availability API")
@asynccontextmanager
async def lifespan(app: FastAPI):
start_scheduler()
yield
stop_scheduler()
app = FastAPI(title="Common Availability API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
@@ -57,7 +69,8 @@ async def create_participant(
await db.refresh(participant)
try:
await sync_participant_calendar(db, participant)
if participant.ics_url:
await sync_participant_calendar(db, participant)
except Exception as e:
logger.warning(f"Initial sync failed for {participant.email}: {e}")
@@ -121,7 +134,57 @@ async def sync_participant(participant_id: UUID, db: AsyncSession = Depends(get_
raise HTTPException(status_code=404, detail="Participant not found")
try:
count = await sync_participant_calendar(db, participant)
return {"status": "success", "blocks_synced": count}
if participant.ics_url:
count = await sync_participant_calendar(db, participant)
return {"status": "success", "blocks_synced": count}
return {"status": "skipped", "message": "No ICS URL provided"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/schedule")
async def schedule_meeting(
data: ScheduleRequest, db: AsyncSession = Depends(get_db)
):
# 1. Validate Lead Time (2 hours)
min_start_time = datetime.now(timezone.utc) + timedelta(hours=2)
if data.start_time.replace(tzinfo=timezone.utc) < min_start_time:
raise HTTPException(
status_code=400,
detail="Meetings must be scheduled at least 2 hours in advance."
)
# 2. Fetch Participants
result = await db.execute(
select(Participant).where(Participant.id.in_(data.participant_ids))
)
participants = result.scalars().all()
if len(participants) != len(data.participant_ids):
raise HTTPException(status_code=400, detail="Some participants not found")
participant_dicts = [
{"name": p.name, "email": p.email} for p in participants
]
participant_names = [p.name for p in participants]
# 3. Send Notifications
email_success = await send_meeting_invite(
participant_dicts,
data.title,
data.description,
data.start_time,
data.end_time
)
zulip_success = send_zulip_notification(
data.title,
data.start_time,
participant_names
)
return {
"status": "success",
"email_sent": email_success,
"zulip_sent": zulip_success
}