Spaces:
Sleeping
Sleeping
| import os | |
| import uvicorn | |
| import requests | |
| import hashlib | |
| import hmac | |
| import time | |
| import logging | |
| from fastapi import FastAPI, Request, HTTPException | |
| # from dotenv import load_dotenv # DEV | |
| # load_dotenv() # DEV | |
| log = logging.getLogger('uvicorn.error') | |
| app = FastAPI() | |
| def home(): | |
| return {"app": "ok"} | |
| async def synthesia_webhook(request: Request): | |
| """ | |
| Webhook to handle Synthesia events. Verifies signature, processes payload, | |
| and forwards data to app API while logging issues for failures. | |
| """ | |
| try: | |
| # Extract and validate headers | |
| request_timestamp = request.headers.get("Synthesia-Timestamp") | |
| request_signature = request.headers.get("Synthesia-Signature") | |
| if not request_timestamp or not request_signature: | |
| raise HTTPException(status_code=401, detail="Unauthorized: Missing required headers") | |
| # Validate the signature | |
| request_body = (await request.body()).decode("utf-8") | |
| message = f"{request_timestamp}.{request_body}" | |
| if not validate_signature(message, request_signature): | |
| raise HTTPException(status_code=401, detail="Unauthorized: Invalid signature") | |
| # Parse and validate payload | |
| request_data = await request.json() | |
| data = request_data.get('data', {}) | |
| title = data.get('title', "Unknown") | |
| video_status = request_data.get('type', 'video.failed') | |
| if not data or video_status not in ['video.failed', 'video.completed']: | |
| log.error(f"Unexpected payload received: {request_data}") | |
| raise HTTPException(status_code=400, detail="Bad request: Invalid payload structure") | |
| # Forward data to app's API | |
| payload = {'data': data, 'video_status': video_status} | |
| status = await forward_video_data(payload) | |
| if status >= 400: | |
| log.warning(f"Failed to forward data for video '{title}' with status {status}") | |
| except HTTPException as http_exc: | |
| log.error(f"HTTP exception occurred: {http_exc.detail}") | |
| raise http_exc | |
| except Exception as e: | |
| log.error(f"Unexpected error: {e}") | |
| raise HTTPException(status_code=500, detail="Internal server error") | |
| return {"message": "ok"} | |
| def get_signature(message): | |
| key = os.getenv('SYNTHESIA_WEBHOOK_SECRET', '') | |
| signature = None | |
| try: | |
| signature = hmac.new( | |
| key.encode("utf-8"), | |
| message.encode("utf-8"), | |
| hashlib.sha256, | |
| ).hexdigest() | |
| except Exception as e: | |
| log.error(f"Error getting signature: {e}") | |
| finally: | |
| return signature | |
| def validate_signature(message, request_signature): | |
| valid = False | |
| try: | |
| system_signature = get_signature(message) | |
| valid = system_signature == request_signature | |
| if not valid: | |
| log.warning("Invalid Synthesia signature!") | |
| except Exception as e: | |
| log.error(f"Error validating Synthesia signature: {e}") | |
| finally: | |
| return valid | |
| async def forward_video_data(payload:dict) -> int: | |
| """ Sends POST requests to create video from template in Synthesia. | |
| :param dict payload: Request payload (title, templateId, templateData) | |
| :returns int status | |
| """ | |
| status = 500 | |
| app_key = os.getenv('APP_KEY', None) | |
| url = os.getenv('APP_URL', None) | |
| headers = { | |
| 'Authorization': f"Bearer {app_key}", | |
| 'Content-Type': 'application/json' | |
| } | |
| title = payload.get('title', '') | |
| try: | |
| response = requests.post(url, json=payload, headers=headers) | |
| status = response.status_code | |
| if response.ok: | |
| data = response.json() | |
| video_id = data.get('id', None) | |
| else: | |
| log.error(f"Failed to create {title}. Status: {status}, Reason: {response.reason}") | |
| except requests.exceptions.ConnectionError as conn_err: | |
| log.error(f"Error creating {title}. Connection error occurred: {conn_err}") | |
| except requests.exceptions.Timeout as timeout_err: | |
| log.error(f"Error creating {title}. Timeout error occurred: {timeout_err}") | |
| except ValueError as json_err: | |
| log.error(f"Error creating {title}. JSON decoding error: {json_err}") | |
| except Exception as e: | |
| log.error(f"Error creating {title}. Error: {e}") | |
| finally: | |
| return status | |