Update New Features

This commit is contained in:
Koncept Kit
2025-12-10 17:52:32 +07:00
parent 005c56b43d
commit f051976881
20 changed files with 2776 additions and 57 deletions

320
ms_calendar_service.py Normal file
View File

@@ -0,0 +1,320 @@
"""
Microsoft Calendar Service
Handles OAuth2 authentication and event synchronization with Microsoft Graph API
"""
from msal import ConfidentialClientApplication
import requests
import os
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from fastapi import HTTPException
class MSCalendarService:
"""
Microsoft Calendar Service using MSAL and Microsoft Graph API
"""
def __init__(self):
"""Initialize MSAL client with credentials from environment"""
self.client_id = os.getenv('MS_CALENDAR_CLIENT_ID')
self.client_secret = os.getenv('MS_CALENDAR_CLIENT_SECRET')
self.tenant_id = os.getenv('MS_CALENDAR_TENANT_ID')
self.redirect_uri = os.getenv('MS_CALENDAR_REDIRECT_URI')
if not all([self.client_id, self.client_secret, self.tenant_id]):
raise ValueError("Microsoft Calendar credentials not properly configured in environment variables")
# Initialize MSAL Confidential Client
self.app = ConfidentialClientApplication(
client_id=self.client_id,
client_credential=self.client_secret,
authority=f"https://login.microsoftonline.com/{self.tenant_id}"
)
# Microsoft Graph API endpoints
self.graph_url = "https://graph.microsoft.com/v1.0"
self.scopes = ["https://graph.microsoft.com/.default"]
def get_access_token(self) -> str:
"""
Get access token using client credentials flow
Returns:
str: Access token for Microsoft Graph API
Raises:
HTTPException: If token acquisition fails
"""
try:
result = self.app.acquire_token_for_client(scopes=self.scopes)
if "access_token" in result:
return result["access_token"]
else:
error = result.get("error_description", "Unknown error")
raise HTTPException(
status_code=500,
detail=f"Failed to acquire access token: {error}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Microsoft authentication error: {str(e)}"
)
def _make_graph_request(
self,
method: str,
endpoint: str,
data: Optional[Dict[Any, Any]] = None
) -> Dict[Any, Any]:
"""
Make an authenticated request to Microsoft Graph API
Args:
method: HTTP method (GET, POST, PATCH, DELETE)
endpoint: API endpoint path (e.g., "/me/events")
data: Request body data
Returns:
Dict: Response JSON
Raises:
HTTPException: If request fails
"""
token = self.get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{self.graph_url}{endpoint}"
try:
if method.upper() == "GET":
response = requests.get(url, headers=headers)
elif method.upper() == "POST":
response = requests.post(url, headers=headers, json=data)
elif method.upper() == "PATCH":
response = requests.patch(url, headers=headers, json=data)
elif method.upper() == "DELETE":
response = requests.delete(url, headers=headers)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
# DELETE requests may return 204 No Content
if response.status_code == 204:
return {}
return response.json()
except requests.exceptions.HTTPError as e:
raise HTTPException(
status_code=e.response.status_code,
detail=f"Microsoft Graph API error: {e.response.text}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Request to Microsoft Graph failed: {str(e)}"
)
async def create_event(
self,
title: str,
description: str,
location: str,
start_at: datetime,
end_at: datetime,
calendar_id: str = "primary"
) -> str:
"""
Create an event in Microsoft Calendar
Args:
title: Event title
description: Event description
location: Event location
start_at: Event start datetime (timezone-aware)
end_at: Event end datetime (timezone-aware)
calendar_id: Calendar ID (default: "primary")
Returns:
str: Microsoft Calendar Event ID
Raises:
HTTPException: If event creation fails
"""
event_data = {
"subject": title,
"body": {
"contentType": "HTML",
"content": description or ""
},
"start": {
"dateTime": start_at.isoformat(),
"timeZone": "UTC"
},
"end": {
"dateTime": end_at.isoformat(),
"timeZone": "UTC"
},
"location": {
"displayName": location
}
}
# Use /me/events for primary calendar or /me/calendars/{id}/events for specific calendar
endpoint = "/me/events" if calendar_id == "primary" else f"/me/calendars/{calendar_id}/events"
result = self._make_graph_request("POST", endpoint, event_data)
return result.get("id")
async def update_event(
self,
event_id: str,
title: Optional[str] = None,
description: Optional[str] = None,
location: Optional[str] = None,
start_at: Optional[datetime] = None,
end_at: Optional[datetime] = None
) -> bool:
"""
Update an existing event in Microsoft Calendar
Args:
event_id: Microsoft Calendar Event ID
title: Updated event title (optional)
description: Updated description (optional)
location: Updated location (optional)
start_at: Updated start datetime (optional)
end_at: Updated end datetime (optional)
Returns:
bool: True if successful
Raises:
HTTPException: If update fails
"""
event_data = {}
if title:
event_data["subject"] = title
if description is not None:
event_data["body"] = {
"contentType": "HTML",
"content": description
}
if location:
event_data["location"] = {"displayName": location}
if start_at:
event_data["start"] = {
"dateTime": start_at.isoformat(),
"timeZone": "UTC"
}
if end_at:
event_data["end"] = {
"dateTime": end_at.isoformat(),
"timeZone": "UTC"
}
if not event_data:
return True # Nothing to update
endpoint = f"/me/events/{event_id}"
self._make_graph_request("PATCH", endpoint, event_data)
return True
async def delete_event(self, event_id: str) -> bool:
"""
Delete an event from Microsoft Calendar
Args:
event_id: Microsoft Calendar Event ID
Returns:
bool: True if successful
Raises:
HTTPException: If deletion fails
"""
endpoint = f"/me/events/{event_id}"
self._make_graph_request("DELETE", endpoint)
return True
async def get_event(self, event_id: str) -> Dict[Any, Any]:
"""
Get event details from Microsoft Calendar
Args:
event_id: Microsoft Calendar Event ID
Returns:
Dict: Event details
Raises:
HTTPException: If retrieval fails
"""
endpoint = f"/me/events/{event_id}"
return self._make_graph_request("GET", endpoint)
async def sync_event(
self,
loaf_event,
existing_ms_event_id: Optional[str] = None
) -> str:
"""
Sync a LOAF event to Microsoft Calendar
Creates new event if existing_ms_event_id is None, otherwise updates
Args:
loaf_event: SQLAlchemy Event model instance
existing_ms_event_id: Existing Microsoft Calendar Event ID (optional)
Returns:
str: Microsoft Calendar Event ID
Raises:
HTTPException: If sync fails
"""
if existing_ms_event_id:
# Update existing event
await self.update_event(
event_id=existing_ms_event_id,
title=loaf_event.title,
description=loaf_event.description,
location=loaf_event.location,
start_at=loaf_event.start_at,
end_at=loaf_event.end_at
)
return existing_ms_event_id
else:
# Create new event
return await self.create_event(
title=loaf_event.title,
description=loaf_event.description or "",
location=loaf_event.location,
start_at=loaf_event.start_at,
end_at=loaf_event.end_at
)
# Singleton instance
_ms_calendar = None
def get_ms_calendar_service() -> MSCalendarService:
"""
Get singleton instance of MSCalendarService
Returns:
MSCalendarService: Initialized Microsoft Calendar service
"""
global _ms_calendar
if _ms_calendar is None:
_ms_calendar = MSCalendarService()
return _ms_calendar