mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 05:57:21 -05:00
This commit adds extensive API integration capabilities for deployment, payments, communications, and monitoring to BlackRoad OS. New API Integrations: - Railway API: Cloud deployment management (GraphQL) - Vercel API: Serverless deployment platform (REST) - Stripe API: Payment processing and billing - Twilio API: SMS, Voice, and WhatsApp messaging - Slack API: Team collaboration and notifications - Discord API: Community messaging and notifications - Sentry API: Error tracking and application monitoring Core Features: - Centralized API client manager with health checking - Comprehensive health monitoring endpoint (/api/health/*) - Automatic retry logic and rate limit handling - Unified status monitoring for all integrations Infrastructure: - Railway deployment configuration (railway.json, railway.toml) - Enhanced GitHub Actions workflows: * backend-tests.yml: Comprehensive test suite with PostgreSQL/Redis * railway-deploy.yml: Automated Railway deployment with notifications - Docker build validation in CI/CD pipeline Testing: - Comprehensive test suite for all API integrations - API connectivity verification in CI/CD - Mock-friendly architecture for testing without credentials Configuration: - Updated .env.example with all new API keys - Added stripe and sentry-sdk to requirements.txt - Registered all new routers in main.py - Updated API info endpoint with new integrations Documentation: - API_INTEGRATIONS.md: Complete setup and usage guide - Interactive API docs at /api/docs with all endpoints - Health check endpoints for monitoring All APIs are optional and gracefully handle missing credentials. The system provides clear status messages for configuration requirements.
260 lines
7.4 KiB
Python
260 lines
7.4 KiB
Python
"""
|
|
Twilio API Integration Router
|
|
|
|
Provides endpoints for SMS, voice calls, and WhatsApp messaging.
|
|
Twilio is a cloud communications platform.
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime
|
|
import httpx
|
|
import base64
|
|
import os
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/twilio", tags=["twilio"])
|
|
|
|
# Twilio API configuration
|
|
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
|
|
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
|
|
TWILIO_PHONE_NUMBER = os.getenv("TWILIO_PHONE_NUMBER")
|
|
|
|
|
|
class SMSMessage(BaseModel):
|
|
"""SMS message model"""
|
|
to: str
|
|
message: str
|
|
from_number: Optional[str] = None
|
|
|
|
|
|
class WhatsAppMessage(BaseModel):
|
|
"""WhatsApp message model"""
|
|
to: str
|
|
message: str
|
|
|
|
|
|
class TwilioClient:
|
|
"""Twilio REST API client"""
|
|
|
|
def __init__(
|
|
self,
|
|
account_sid: Optional[str] = None,
|
|
auth_token: Optional[str] = None,
|
|
phone_number: Optional[str] = None
|
|
):
|
|
self.account_sid = account_sid or TWILIO_ACCOUNT_SID
|
|
self.auth_token = auth_token or TWILIO_AUTH_TOKEN
|
|
self.phone_number = phone_number or TWILIO_PHONE_NUMBER
|
|
self.base_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
|
|
|
|
def _get_headers(self) -> Dict[str, str]:
|
|
"""Get API request headers with basic auth"""
|
|
if not self.account_sid or not self.auth_token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Twilio credentials not configured"
|
|
)
|
|
|
|
# Create basic auth header
|
|
credentials = f"{self.account_sid}:{self.auth_token}"
|
|
encoded = base64.b64encode(credentials.encode()).decode()
|
|
|
|
return {
|
|
"Authorization": f"Basic {encoded}",
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
}
|
|
|
|
async def _request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
data: Optional[Dict] = None
|
|
) -> Dict[str, Any]:
|
|
"""Make API request"""
|
|
headers = self._get_headers()
|
|
url = f"{self.base_url}{endpoint}"
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.request(
|
|
method,
|
|
url,
|
|
headers=headers,
|
|
data=data,
|
|
timeout=30.0
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
logger.error(f"Twilio API error: {e.response.text}")
|
|
raise HTTPException(
|
|
status_code=e.response.status_code,
|
|
detail=f"Twilio API error: {e.response.text}"
|
|
)
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"Twilio API request failed: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"Twilio API request failed: {str(e)}"
|
|
)
|
|
|
|
async def send_sms(
|
|
self,
|
|
to: str,
|
|
body: str,
|
|
from_: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""Send SMS message"""
|
|
data = {
|
|
"To": to,
|
|
"From": from_ or self.phone_number,
|
|
"Body": body
|
|
}
|
|
|
|
if not data["From"]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Twilio phone number not configured"
|
|
)
|
|
|
|
return await self._request("POST", "/Messages.json", data=data)
|
|
|
|
async def send_whatsapp(
|
|
self,
|
|
to: str,
|
|
body: str
|
|
) -> Dict[str, Any]:
|
|
"""Send WhatsApp message"""
|
|
data = {
|
|
"To": f"whatsapp:{to}",
|
|
"From": f"whatsapp:{self.phone_number}",
|
|
"Body": body
|
|
}
|
|
|
|
if not self.phone_number:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Twilio phone number not configured"
|
|
)
|
|
|
|
return await self._request("POST", "/Messages.json", data=data)
|
|
|
|
async def get_message(self, message_sid: str) -> Dict[str, Any]:
|
|
"""Get message details"""
|
|
return await self._request("GET", f"/Messages/{message_sid}.json")
|
|
|
|
async def list_messages(self, limit: int = 20) -> Dict[str, Any]:
|
|
"""List messages"""
|
|
data = {"PageSize": limit}
|
|
return await self._request("GET", "/Messages.json", data=data)
|
|
|
|
|
|
# Initialize client
|
|
twilio_client = TwilioClient()
|
|
|
|
|
|
@router.get("/status")
|
|
async def get_twilio_status():
|
|
"""Get Twilio API connection status"""
|
|
if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN:
|
|
return {
|
|
"connected": False,
|
|
"message": "Twilio credentials not configured. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN."
|
|
}
|
|
|
|
try:
|
|
# Try to list messages as a health check
|
|
await twilio_client.list_messages(limit=1)
|
|
return {
|
|
"connected": True,
|
|
"message": "Twilio API connected successfully",
|
|
"phone_number_configured": bool(TWILIO_PHONE_NUMBER)
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"connected": False,
|
|
"message": f"Twilio API connection failed: {str(e)}"
|
|
}
|
|
|
|
|
|
@router.post("/sms")
|
|
async def send_sms(message: SMSMessage):
|
|
"""Send SMS message"""
|
|
try:
|
|
result = await twilio_client.send_sms(
|
|
to=message.to,
|
|
body=message.message,
|
|
from_=message.from_number
|
|
)
|
|
return {
|
|
"success": True,
|
|
"message": result,
|
|
"sid": result.get("sid")
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error sending SMS: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to send SMS: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/whatsapp")
|
|
async def send_whatsapp(message: WhatsAppMessage):
|
|
"""Send WhatsApp message"""
|
|
try:
|
|
result = await twilio_client.send_whatsapp(
|
|
to=message.to,
|
|
body=message.message
|
|
)
|
|
return {
|
|
"success": True,
|
|
"message": result,
|
|
"sid": result.get("sid")
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error sending WhatsApp: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to send WhatsApp: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/messages/{message_sid}")
|
|
async def get_message(message_sid: str):
|
|
"""Get message details"""
|
|
try:
|
|
message = await twilio_client.get_message(message_sid)
|
|
return message
|
|
except HTTPException:
|
|
raise
|
|
|
|
|
|
@router.get("/messages")
|
|
async def list_messages(limit: int = 20):
|
|
"""List messages"""
|
|
try:
|
|
result = await twilio_client.list_messages(limit)
|
|
return result
|
|
except HTTPException:
|
|
raise
|
|
|
|
|
|
@router.get("/health")
|
|
async def twilio_health_check():
|
|
"""Twilio API health check endpoint"""
|
|
return {
|
|
"service": "twilio",
|
|
"status": "operational" if (TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN) else "not_configured",
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|