Add comprehensive multi-API integration support

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.
This commit is contained in:
Claude
2025-11-16 09:34:14 +00:00
parent b9eab34af4
commit 84ab793177
18 changed files with 4145 additions and 3 deletions

View File

@@ -0,0 +1,387 @@
"""
API Health Check Router
Comprehensive health check endpoint for all external API integrations.
Provides status monitoring for Railway, Vercel, Stripe, Twilio, Slack, Discord, Sentry, and more.
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Dict, List, Optional, Any
from datetime import datetime
import asyncio
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/health", tags=["health"])
class APIHealthStatus(BaseModel):
"""Health status for a single API"""
name: str
status: str # connected, not_configured, error
message: str
last_checked: str
configuration: Dict[str, bool]
error: Optional[str] = None
class SystemHealthStatus(BaseModel):
"""Overall system health status"""
status: str # healthy, degraded, unhealthy
timestamp: str
total_apis: int
connected_apis: int
not_configured_apis: int
error_apis: int
apis: Dict[str, APIHealthStatus]
async def check_api_status(name: str, check_func) -> Dict[str, Any]:
"""Check individual API status"""
try:
result = await check_func()
return {
"name": name,
"status": "connected" if result.get("connected") else "not_configured",
"message": result.get("message", ""),
"last_checked": datetime.utcnow().isoformat(),
"configuration": {
k: v for k, v in result.items()
if k.endswith("_configured") or k == "connected"
},
"error": None
}
except Exception as e:
logger.error(f"Health check failed for {name}: {e}")
return {
"name": name,
"status": "error",
"message": f"Health check failed: {str(e)}",
"last_checked": datetime.utcnow().isoformat(),
"configuration": {},
"error": str(e)
}
@router.get("/all", response_model=SystemHealthStatus)
async def check_all_apis():
"""
Comprehensive health check for all external APIs.
Checks connectivity and configuration for:
- GitHub API
- Railway API
- Vercel API
- Stripe API
- Twilio API (SMS & WhatsApp)
- Slack API
- Discord API
- Sentry API
- OpenAI API
- Hugging Face API
- DigitalOcean API
- AWS S3
"""
# Import API clients
from .railway import get_railway_status
from .vercel import get_vercel_status
from .stripe import get_stripe_status
from .twilio import get_twilio_status
from .slack import get_slack_status
from .discord import get_discord_status
from .sentry import get_sentry_status
# Define all API checks
api_checks = {
"railway": get_railway_status,
"vercel": get_vercel_status,
"stripe": get_stripe_status,
"twilio": get_twilio_status,
"slack": get_slack_status,
"discord": get_discord_status,
"sentry": get_sentry_status,
}
# Add checks for existing APIs
api_checks.update({
"github": lambda: check_github_status(),
"openai": lambda: check_openai_status(),
"huggingface": lambda: check_huggingface_status(),
"digitalocean": lambda: check_digitalocean_status(),
"aws": lambda: check_aws_status(),
})
# Run all checks concurrently
tasks = [
check_api_status(name, func)
for name, func in api_checks.items()
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Process results
apis = {}
connected_count = 0
not_configured_count = 0
error_count = 0
for result in results:
if isinstance(result, Exception):
error_count += 1
continue
apis[result["name"]] = result
if result["status"] == "connected":
connected_count += 1
elif result["status"] == "not_configured":
not_configured_count += 1
else:
error_count += 1
# Determine overall system health
total_apis = len(apis)
if connected_count == total_apis:
overall_status = "healthy"
elif connected_count > 0:
overall_status = "degraded"
else:
overall_status = "unhealthy"
return SystemHealthStatus(
status=overall_status,
timestamp=datetime.utcnow().isoformat(),
total_apis=total_apis,
connected_apis=connected_count,
not_configured_apis=not_configured_count,
error_apis=error_count,
apis=apis
)
@router.get("/summary")
async def get_health_summary():
"""Get a quick summary of API health"""
health = await check_all_apis()
return {
"status": health.status,
"timestamp": health.timestamp,
"summary": {
"total": health.total_apis,
"connected": health.connected_apis,
"not_configured": health.not_configured_apis,
"errors": health.error_apis
},
"connected_apis": [
name for name, api in health.apis.items()
if api.status == "connected"
],
"not_configured_apis": [
name for name, api in health.apis.items()
if api.status == "not_configured"
],
"error_apis": [
name for name, api in health.apis.items()
if api.status == "error"
]
}
@router.get("/{api_name}")
async def check_specific_api(api_name: str):
"""Check health of a specific API"""
api_checks = {
"railway": lambda: __import__("app.routers.railway", fromlist=["get_railway_status"]).get_railway_status(),
"vercel": lambda: __import__("app.routers.vercel", fromlist=["get_vercel_status"]).get_vercel_status(),
"stripe": lambda: __import__("app.routers.stripe", fromlist=["get_stripe_status"]).get_stripe_status(),
"twilio": lambda: __import__("app.routers.twilio", fromlist=["get_twilio_status"]).get_twilio_status(),
"slack": lambda: __import__("app.routers.slack", fromlist=["get_slack_status"]).get_slack_status(),
"discord": lambda: __import__("app.routers.discord", fromlist=["get_discord_status"]).get_discord_status(),
"sentry": lambda: __import__("app.routers.sentry", fromlist=["get_sentry_status"]).get_sentry_status(),
"github": check_github_status,
"openai": check_openai_status,
"huggingface": check_huggingface_status,
"digitalocean": check_digitalocean_status,
"aws": check_aws_status,
}
if api_name.lower() not in api_checks:
raise HTTPException(
status_code=404,
detail=f"API '{api_name}' not found. Available APIs: {', '.join(api_checks.keys())}"
)
check_func = api_checks[api_name.lower()]
result = await check_api_status(api_name, check_func)
return result
# Helper functions for existing APIs
async def check_github_status():
"""Check GitHub API status"""
github_token = os.getenv("GITHUB_TOKEN")
if not github_token:
return {
"connected": False,
"message": "GitHub token not configured",
"token_configured": False
}
import httpx
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.github.com/user",
headers={"Authorization": f"token {github_token}"},
timeout=10.0
)
response.raise_for_status()
return {
"connected": True,
"message": "GitHub API connected successfully",
"token_configured": True
}
except Exception as e:
return {
"connected": False,
"message": f"GitHub API connection failed: {str(e)}",
"token_configured": True
}
async def check_openai_status():
"""Check OpenAI API status"""
openai_key = os.getenv("OPENAI_API_KEY")
if not openai_key:
return {
"connected": False,
"message": "OpenAI API key not configured",
"key_configured": False
}
import httpx
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.openai.com/v1/models",
headers={"Authorization": f"Bearer {openai_key}"},
timeout=10.0
)
response.raise_for_status()
return {
"connected": True,
"message": "OpenAI API connected successfully",
"key_configured": True
}
except Exception as e:
return {
"connected": False,
"message": f"OpenAI API connection failed: {str(e)}",
"key_configured": True
}
async def check_huggingface_status():
"""Check Hugging Face API status"""
hf_token = os.getenv("HUGGINGFACE_TOKEN")
if not hf_token:
return {
"connected": False,
"message": "Hugging Face token not configured",
"token_configured": False
}
import httpx
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://huggingface.co/api/whoami-v2",
headers={"Authorization": f"Bearer {hf_token}"},
timeout=10.0
)
response.raise_for_status()
return {
"connected": True,
"message": "Hugging Face API connected successfully",
"token_configured": True
}
except Exception as e:
return {
"connected": False,
"message": f"Hugging Face API connection failed: {str(e)}",
"token_configured": True
}
async def check_digitalocean_status():
"""Check DigitalOcean API status"""
do_token = os.getenv("DIGITALOCEAN_TOKEN")
if not do_token:
return {
"connected": False,
"message": "DigitalOcean token not configured",
"token_configured": False
}
import httpx
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.digitalocean.com/v2/account",
headers={"Authorization": f"Bearer {do_token}"},
timeout=10.0
)
response.raise_for_status()
return {
"connected": True,
"message": "DigitalOcean API connected successfully",
"token_configured": True
}
except Exception as e:
return {
"connected": False,
"message": f"DigitalOcean API connection failed: {str(e)}",
"token_configured": True
}
async def check_aws_status():
"""Check AWS S3 status"""
aws_key = os.getenv("AWS_ACCESS_KEY_ID")
aws_secret = os.getenv("AWS_SECRET_ACCESS_KEY")
if not aws_key or not aws_secret:
return {
"connected": False,
"message": "AWS credentials not configured",
"key_configured": bool(aws_key),
"secret_configured": bool(aws_secret)
}
try:
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client('s3')
s3.list_buckets()
return {
"connected": True,
"message": "AWS S3 connected successfully",
"key_configured": True,
"secret_configured": True
}
except Exception as e:
return {
"connected": False,
"message": f"AWS S3 connection failed: {str(e)}",
"key_configured": True,
"secret_configured": True
}

View File

@@ -0,0 +1,308 @@
"""
Discord API Integration Router
Provides endpoints for sending messages, managing channels, and interacting with Discord servers.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/discord", tags=["discord"])
# Discord API configuration
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
class DiscordMessage(BaseModel):
"""Discord message model"""
content: str
embeds: Optional[List[Dict]] = None
username: Optional[str] = None
avatar_url: Optional[str] = None
class DiscordEmbed(BaseModel):
"""Discord embed model"""
title: Optional[str] = None
description: Optional[str] = None
color: Optional[int] = 0x00ff00
url: Optional[str] = None
timestamp: Optional[str] = None
fields: Optional[List[Dict]] = None
class DiscordClient:
"""Discord REST API client"""
def __init__(self, token: Optional[str] = None):
self.token = token or DISCORD_BOT_TOKEN
self.base_url = "https://discord.com/api/v10"
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Discord bot token not configured"
)
return {
"Authorization": f"Bot {self.token}",
"Content-Type": "application/json"
}
async def _request(
self,
method: str,
endpoint: str,
json_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,
json=json_data,
timeout=30.0
)
response.raise_for_status()
# Some endpoints return 204 No Content
if response.status_code == 204:
return {"success": True}
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Discord API error: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Discord API error: {e.response.text}"
)
except httpx.HTTPError as e:
logger.error(f"Discord API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Discord API request failed: {str(e)}"
)
async def send_message(
self,
channel_id: str,
content: str,
embeds: Optional[List[Dict]] = None
) -> Dict[str, Any]:
"""Send a message to a channel"""
data = {"content": content}
if embeds:
data["embeds"] = embeds
return await self._request("POST", f"/channels/{channel_id}/messages", json_data=data)
async def get_channel(self, channel_id: str) -> Dict[str, Any]:
"""Get channel information"""
return await self._request("GET", f"/channels/{channel_id}")
async def get_guild(self, guild_id: str) -> Dict[str, Any]:
"""Get guild (server) information"""
return await self._request("GET", f"/guilds/{guild_id}")
async def list_guild_channels(self, guild_id: str) -> List[Dict[str, Any]]:
"""List channels in a guild"""
return await self._request("GET", f"/guilds/{guild_id}/channels")
async def get_user(self, user_id: str) -> Dict[str, Any]:
"""Get user information"""
return await self._request("GET", f"/users/{user_id}")
async def get_current_user(self) -> Dict[str, Any]:
"""Get current bot user information"""
return await self._request("GET", "/users/@me")
async def send_webhook_message(
content: str,
embeds: Optional[List[Dict]] = None,
username: Optional[str] = "BlackRoad OS",
avatar_url: Optional[str] = None
):
"""Send message via webhook (doesn't require bot token)"""
if not DISCORD_WEBHOOK_URL:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Discord webhook URL not configured"
)
data = {
"content": content,
"username": username
}
if embeds:
data["embeds"] = embeds
if avatar_url:
data["avatar_url"] = avatar_url
async with httpx.AsyncClient() as client:
try:
response = await client.post(
DISCORD_WEBHOOK_URL,
json=data,
timeout=10.0
)
response.raise_for_status()
return {"success": True, "message": "Message sent via webhook"}
except httpx.HTTPError as e:
logger.error(f"Discord webhook error: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Discord webhook failed: {str(e)}"
)
# Initialize client
discord_client = DiscordClient()
@router.get("/status")
async def get_discord_status():
"""Get Discord API connection status"""
if not DISCORD_BOT_TOKEN:
return {
"connected": False,
"message": "Discord bot token not configured. Set DISCORD_BOT_TOKEN environment variable.",
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
}
try:
# Test API connection by getting bot user info
user = await discord_client.get_current_user()
return {
"connected": True,
"message": "Discord API connected successfully",
"bot_username": user.get("username"),
"bot_id": user.get("id"),
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
}
except Exception as e:
return {
"connected": False,
"message": f"Discord API connection failed: {str(e)}",
"webhook_configured": bool(DISCORD_WEBHOOK_URL)
}
@router.post("/channels/{channel_id}/messages")
async def send_message(channel_id: str, content: str, embeds: Optional[List[Dict]] = None):
"""Send a message to a Discord channel"""
try:
result = await discord_client.send_message(
channel_id=channel_id,
content=content,
embeds=embeds
)
return {
"success": True,
"message_id": result.get("id"),
"channel_id": result.get("channel_id")
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error sending message: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to send message: {str(e)}"
)
@router.post("/webhook")
async def send_webhook(message: DiscordMessage):
"""Send message via incoming webhook"""
try:
result = await send_webhook_message(
content=message.content,
embeds=message.embeds,
username=message.username or "BlackRoad OS",
avatar_url=message.avatar_url
)
return result
except HTTPException:
raise
@router.get("/channels/{channel_id}")
async def get_channel(channel_id: str):
"""Get channel information"""
try:
channel = await discord_client.get_channel(channel_id)
return channel
except HTTPException:
raise
@router.get("/guilds/{guild_id}")
async def get_guild(guild_id: str):
"""Get guild (server) information"""
try:
guild = await discord_client.get_guild(guild_id)
return guild
except HTTPException:
raise
@router.get("/guilds/{guild_id}/channels")
async def list_channels(guild_id: str):
"""List channels in a guild"""
try:
channels = await discord_client.list_guild_channels(guild_id)
return {
"channels": channels,
"count": len(channels)
}
except HTTPException:
raise
@router.get("/users/{user_id}")
async def get_user(user_id: str):
"""Get user information"""
try:
user = await discord_client.get_user(user_id)
return user
except HTTPException:
raise
@router.get("/users/@me")
async def get_current_user():
"""Get current bot user information"""
try:
user = await discord_client.get_current_user()
return user
except HTTPException:
raise
@router.get("/health")
async def discord_health_check():
"""Discord API health check endpoint"""
return {
"service": "discord",
"status": "operational" if DISCORD_BOT_TOKEN else "not_configured",
"webhook_status": "operational" if DISCORD_WEBHOOK_URL else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,393 @@
"""
Railway API Integration Router
Provides endpoints for managing Railway deployments, projects, and services.
Railway is a deployment platform that simplifies infrastructure management.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/railway", tags=["railway"])
# Railway API configuration
RAILWAY_API_URL = "https://backboard.railway.app/graphql"
RAILWAY_TOKEN = os.getenv("RAILWAY_TOKEN")
class RailwayProject(BaseModel):
"""Railway project model"""
id: str
name: str
description: Optional[str] = None
created_at: datetime
updated_at: datetime
class RailwayService(BaseModel):
"""Railway service model"""
id: str
name: str
project_id: str
status: str
created_at: datetime
class RailwayDeployment(BaseModel):
"""Railway deployment model"""
id: str
service_id: str
status: str
created_at: datetime
url: Optional[str] = None
environment: str = "production"
class DeploymentCreate(BaseModel):
"""Create deployment request"""
project_id: str
service_id: str
environment: Optional[str] = "production"
class RailwayVariable(BaseModel):
"""Environment variable"""
key: str
value: str
class RailwayClient:
"""Railway GraphQL API client"""
def __init__(self, token: Optional[str] = None):
self.token = token or RAILWAY_TOKEN
self.api_url = RAILWAY_API_URL
async def _graphql_request(self, query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
"""Execute GraphQL request"""
if not self.token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Railway API token not configured"
)
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
payload = {
"query": query,
"variables": variables or {}
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(
self.api_url,
json=payload,
headers=headers,
timeout=30.0
)
response.raise_for_status()
data = response.json()
if "errors" in data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"GraphQL error: {data['errors']}"
)
return data.get("data", {})
except httpx.HTTPError as e:
logger.error(f"Railway API error: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Railway API request failed: {str(e)}"
)
async def get_projects(self) -> List[Dict[str, Any]]:
"""Get all projects"""
query = """
query {
projects {
edges {
node {
id
name
description
createdAt
updatedAt
}
}
}
}
"""
result = await self._graphql_request(query)
edges = result.get("projects", {}).get("edges", [])
return [edge["node"] for edge in edges]
async def get_project(self, project_id: str) -> Optional[Dict[str, Any]]:
"""Get project by ID"""
query = """
query($id: String!) {
project(id: $id) {
id
name
description
createdAt
updatedAt
}
}
"""
result = await self._graphql_request(query, {"id": project_id})
return result.get("project")
async def get_services(self, project_id: str) -> List[Dict[str, Any]]:
"""Get services for a project"""
query = """
query($projectId: String!) {
project(id: $projectId) {
services {
edges {
node {
id
name
createdAt
}
}
}
}
}
"""
result = await self._graphql_request(query, {"projectId": project_id})
edges = result.get("project", {}).get("services", {}).get("edges", [])
return [edge["node"] for edge in edges]
async def get_deployments(self, service_id: str) -> List[Dict[str, Any]]:
"""Get deployments for a service"""
query = """
query($serviceId: String!) {
service(id: $serviceId) {
deployments {
edges {
node {
id
status
createdAt
url
}
}
}
}
}
"""
result = await self._graphql_request(query, {"serviceId": service_id})
edges = result.get("service", {}).get("deployments", {}).get("edges", [])
return [edge["node"] for edge in edges]
async def trigger_deployment(self, service_id: str) -> Dict[str, Any]:
"""Trigger a new deployment"""
query = """
mutation($serviceId: String!) {
serviceDeploy(serviceId: $serviceId) {
id
status
createdAt
}
}
"""
result = await self._graphql_request(query, {"serviceId": service_id})
return result.get("serviceDeploy", {})
async def set_variables(
self,
project_id: str,
environment_id: str,
variables: Dict[str, str]
) -> bool:
"""Set environment variables"""
query = """
mutation($projectId: String!, $environmentId: String!, $variables: String!) {
variableCollectionUpsert(
input: {
projectId: $projectId
environmentId: $environmentId
variables: $variables
}
)
}
"""
import json
variables_json = json.dumps(variables)
result = await self._graphql_request(
query,
{
"projectId": project_id,
"environmentId": environment_id,
"variables": variables_json
}
)
return bool(result.get("variableCollectionUpsert"))
# Initialize client
railway_client = RailwayClient()
@router.get("/status")
async def get_railway_status():
"""Get Railway API connection status"""
if not RAILWAY_TOKEN:
return {
"connected": False,
"message": "Railway API token not configured. Set RAILWAY_TOKEN environment variable."
}
try:
# Try to fetch projects as a health check
projects = await railway_client.get_projects()
return {
"connected": True,
"message": "Railway API connected successfully",
"project_count": len(projects)
}
except Exception as e:
return {
"connected": False,
"message": f"Railway API connection failed: {str(e)}"
}
@router.get("/projects", response_model=List[Dict[str, Any]])
async def list_projects():
"""List all Railway projects"""
try:
projects = await railway_client.get_projects()
return projects
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching projects: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch projects: {str(e)}"
)
@router.get("/projects/{project_id}")
async def get_project(project_id: str):
"""Get project details"""
project = await railway_client.get_project(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Project not found"
)
return project
@router.get("/projects/{project_id}/services")
async def list_services(project_id: str):
"""List services in a project"""
try:
services = await railway_client.get_services(project_id)
return services
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching services: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch services: {str(e)}"
)
@router.get("/services/{service_id}/deployments")
async def list_deployments(service_id: str):
"""List deployments for a service"""
try:
deployments = await railway_client.get_deployments(service_id)
return deployments
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching deployments: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch deployments: {str(e)}"
)
@router.post("/services/{service_id}/deploy")
async def deploy_service(service_id: str):
"""Trigger a new deployment for a service"""
try:
deployment = await railway_client.trigger_deployment(service_id)
return {
"success": True,
"deployment": deployment,
"message": "Deployment triggered successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error triggering deployment: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to trigger deployment: {str(e)}"
)
@router.post("/projects/{project_id}/variables")
async def update_variables(
project_id: str,
environment_id: str,
variables: List[RailwayVariable]
):
"""Update environment variables for a project"""
try:
variables_dict = {var.key: var.value for var in variables}
success = await railway_client.set_variables(
project_id,
environment_id,
variables_dict
)
if success:
return {
"success": True,
"message": "Variables updated successfully"
}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to update variables"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating variables: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update variables: {str(e)}"
)
@router.get("/health")
async def railway_health_check():
"""Railway API health check endpoint"""
return {
"service": "railway",
"status": "operational" if RAILWAY_TOKEN else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,380 @@
"""
Sentry Error Tracking Integration Router
Provides endpoints for error tracking, performance monitoring, and release management.
Sentry is an application monitoring and error tracking platform.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/sentry", tags=["sentry"])
# Sentry API configuration
SENTRY_AUTH_TOKEN = os.getenv("SENTRY_AUTH_TOKEN")
SENTRY_ORG = os.getenv("SENTRY_ORG")
SENTRY_DSN = os.getenv("SENTRY_DSN")
class SentryError(BaseModel):
"""Sentry error/event model"""
message: str
level: str = "error" # debug, info, warning, error, fatal
tags: Optional[Dict[str, str]] = None
extra: Optional[Dict[str, Any]] = None
class SentryRelease(BaseModel):
"""Sentry release model"""
version: str
projects: List[str]
ref: Optional[str] = None
class SentryClient:
"""Sentry REST API client"""
def __init__(
self,
auth_token: Optional[str] = None,
org: Optional[str] = None
):
self.auth_token = auth_token or SENTRY_AUTH_TOKEN
self.org = org or SENTRY_ORG
self.base_url = "https://sentry.io/api/0"
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.auth_token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Sentry auth token not configured"
)
return {
"Authorization": f"Bearer {self.auth_token}",
"Content-Type": "application/json"
}
async def _request(
self,
method: str,
endpoint: str,
json_data: Optional[Dict] = None,
params: 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,
json=json_data,
params=params,
timeout=30.0
)
response.raise_for_status()
# Handle 204 No Content
if response.status_code == 204:
return {"success": True}
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Sentry API error: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Sentry API error: {e.response.text}"
)
except httpx.HTTPError as e:
logger.error(f"Sentry API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Sentry API request failed: {str(e)}"
)
async def get_projects(self) -> List[Dict[str, Any]]:
"""Get all projects in organization"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
return await self._request("GET", f"/organizations/{self.org}/projects/")
async def get_issues(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""Get issues for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/issues/",
params=params
)
async def get_events(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""Get events for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/events/",
params=params
)
async def create_release(
self,
version: str,
projects: List[str],
ref: Optional[str] = None
) -> Dict[str, Any]:
"""Create a new release"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
data = {
"version": version,
"projects": projects
}
if ref:
data["ref"] = ref
return await self._request(
"POST",
f"/organizations/{self.org}/releases/",
json_data=data
)
async def list_releases(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""List releases for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/releases/",
params=params
)
async def get_stats(
self,
project: str,
stat: str = "received"
) -> List[Dict[str, Any]]:
"""Get project statistics"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"stat": stat}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/stats/",
params=params
)
# Initialize client
sentry_client = SentryClient()
@router.get("/status")
async def get_sentry_status():
"""Get Sentry API connection status"""
if not SENTRY_AUTH_TOKEN:
return {
"connected": False,
"message": "Sentry auth token not configured. Set SENTRY_AUTH_TOKEN environment variable.",
"org_configured": bool(SENTRY_ORG),
"dsn_configured": bool(SENTRY_DSN)
}
try:
# Test API connection
projects = await sentry_client.get_projects()
return {
"connected": True,
"message": "Sentry API connected successfully",
"organization": SENTRY_ORG,
"project_count": len(projects),
"dsn_configured": bool(SENTRY_DSN)
}
except Exception as e:
return {
"connected": False,
"message": f"Sentry API connection failed: {str(e)}",
"org_configured": bool(SENTRY_ORG),
"dsn_configured": bool(SENTRY_DSN)
}
@router.get("/projects")
async def list_projects():
"""List all Sentry projects"""
try:
projects = await sentry_client.get_projects()
return {
"projects": projects,
"count": len(projects)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching projects: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch projects: {str(e)}"
)
@router.get("/projects/{project}/issues")
async def list_issues(project: str, limit: int = 25):
"""List issues for a project"""
try:
issues = await sentry_client.get_issues(project, limit)
return {
"issues": issues,
"count": len(issues)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching issues: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch issues: {str(e)}"
)
@router.get("/projects/{project}/events")
async def list_events(project: str, limit: int = 25):
"""List events for a project"""
try:
events = await sentry_client.get_events(project, limit)
return {
"events": events,
"count": len(events)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching events: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch events: {str(e)}"
)
@router.post("/releases")
async def create_release(release: SentryRelease):
"""Create a new release"""
try:
result = await sentry_client.create_release(
version=release.version,
projects=release.projects,
ref=release.ref
)
return {
"success": True,
"release": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating release: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create release: {str(e)}"
)
@router.get("/projects/{project}/releases")
async def list_releases(project: str, limit: int = 25):
"""List releases for a project"""
try:
releases = await sentry_client.list_releases(project, limit)
return {
"releases": releases,
"count": len(releases)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching releases: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch releases: {str(e)}"
)
@router.get("/projects/{project}/stats")
async def get_stats(project: str, stat: str = "received"):
"""Get project statistics"""
try:
stats = await sentry_client.get_stats(project, stat)
return {
"stats": stats,
"stat_type": stat
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching stats: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch stats: {str(e)}"
)
@router.get("/health")
async def sentry_health_check():
"""Sentry API health check endpoint"""
return {
"service": "sentry",
"status": "operational" if SENTRY_AUTH_TOKEN else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,281 @@
"""
Slack API Integration Router
Provides endpoints for sending messages, managing channels, and interacting with Slack workspaces.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/slack", tags=["slack"])
# Slack API configuration
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
class SlackMessage(BaseModel):
"""Slack message model"""
channel: str
text: str
blocks: Optional[List[Dict]] = None
thread_ts: Optional[str] = None
class WebhookMessage(BaseModel):
"""Webhook message model"""
text: str
username: Optional[str] = "BlackRoad OS"
icon_emoji: Optional[str] = ":robot_face:"
class SlackClient:
"""Slack Web API client"""
def __init__(self, token: Optional[str] = None):
self.token = token or SLACK_BOT_TOKEN
self.base_url = "https://slack.com/api"
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Slack bot token not configured"
)
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
async def _request(
self,
method: str,
endpoint: str,
json_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,
json=json_data,
timeout=30.0
)
response.raise_for_status()
data = response.json()
# Slack returns 200 with ok:false for errors
if not data.get("ok", False):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Slack API error: {data.get('error', 'Unknown error')}"
)
return data
except httpx.HTTPStatusError as e:
logger.error(f"Slack API error: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Slack API error: {e.response.text}"
)
except httpx.HTTPError as e:
logger.error(f"Slack API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Slack API request failed: {str(e)}"
)
async def post_message(
self,
channel: str,
text: str,
blocks: Optional[List[Dict]] = None,
thread_ts: Optional[str] = None
) -> Dict[str, Any]:
"""Post a message to a channel"""
data = {
"channel": channel,
"text": text
}
if blocks:
data["blocks"] = blocks
if thread_ts:
data["thread_ts"] = thread_ts
return await self._request("POST", "chat.postMessage", json_data=data)
async def list_channels(self) -> Dict[str, Any]:
"""List public channels"""
return await self._request("GET", "conversations.list")
async def get_user_info(self, user_id: str) -> Dict[str, Any]:
"""Get user information"""
return await self._request("POST", "users.info", json_data={"user": user_id})
async def upload_file(
self,
channels: str,
content: str,
filename: str,
title: Optional[str] = None
) -> Dict[str, Any]:
"""Upload a file"""
data = {
"channels": channels,
"content": content,
"filename": filename
}
if title:
data["title"] = title
return await self._request("POST", "files.upload", json_data=data)
async def send_webhook_message(text: str, username: str = "BlackRoad OS", icon_emoji: str = ":robot_face:"):
"""Send message via webhook (doesn't require bot token)"""
if not SLACK_WEBHOOK_URL:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Slack webhook URL not configured"
)
async with httpx.AsyncClient() as client:
try:
response = await client.post(
SLACK_WEBHOOK_URL,
json={
"text": text,
"username": username,
"icon_emoji": icon_emoji
},
timeout=10.0
)
response.raise_for_status()
return {"success": True, "message": "Message sent via webhook"}
except httpx.HTTPError as e:
logger.error(f"Slack webhook error: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Slack webhook failed: {str(e)}"
)
# Initialize client
slack_client = SlackClient()
@router.get("/status")
async def get_slack_status():
"""Get Slack API connection status"""
if not SLACK_BOT_TOKEN:
return {
"connected": False,
"message": "Slack bot token not configured. Set SLACK_BOT_TOKEN environment variable.",
"webhook_configured": bool(SLACK_WEBHOOK_URL)
}
try:
# Test API connection
result = await slack_client._request("POST", "auth.test")
return {
"connected": True,
"message": "Slack API connected successfully",
"team": result.get("team"),
"user": result.get("user"),
"webhook_configured": bool(SLACK_WEBHOOK_URL)
}
except Exception as e:
return {
"connected": False,
"message": f"Slack API connection failed: {str(e)}",
"webhook_configured": bool(SLACK_WEBHOOK_URL)
}
@router.post("/messages")
async def post_message(message: SlackMessage):
"""Post a message to a Slack channel"""
try:
result = await slack_client.post_message(
channel=message.channel,
text=message.text,
blocks=message.blocks,
thread_ts=message.thread_ts
)
return {
"success": True,
"ts": result.get("ts"),
"channel": result.get("channel")
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error posting message: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to post message: {str(e)}"
)
@router.post("/webhook")
async def send_webhook(message: WebhookMessage):
"""Send message via incoming webhook"""
try:
result = await send_webhook_message(
text=message.text,
username=message.username,
icon_emoji=message.icon_emoji
)
return result
except HTTPException:
raise
@router.get("/channels")
async def list_channels():
"""List Slack channels"""
try:
result = await slack_client.list_channels()
return {
"channels": result.get("channels", []),
"count": len(result.get("channels", []))
}
except HTTPException:
raise
@router.get("/users/{user_id}")
async def get_user(user_id: str):
"""Get user information"""
try:
result = await slack_client.get_user_info(user_id)
return result.get("user", {})
except HTTPException:
raise
@router.get("/health")
async def slack_health_check():
"""Slack API health check endpoint"""
return {
"service": "slack",
"status": "operational" if SLACK_BOT_TOKEN else "not_configured",
"webhook_status": "operational" if SLACK_WEBHOOK_URL else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,328 @@
"""
Stripe API Integration Router
Provides endpoints for payment processing, subscriptions, and billing.
Stripe is a payment processing platform for online businesses.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/stripe", tags=["stripe"])
# Stripe API configuration
STRIPE_API_URL = "https://api.stripe.com/v1"
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
STRIPE_PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY")
class PaymentIntent(BaseModel):
"""Payment intent model"""
amount: int # Amount in cents
currency: str = "usd"
description: Optional[str] = None
metadata: Optional[Dict[str, str]] = None
class Customer(BaseModel):
"""Customer model"""
email: EmailStr
name: Optional[str] = None
description: Optional[str] = None
metadata: Optional[Dict[str, str]] = None
class StripeClient:
"""Stripe REST API client"""
def __init__(self, secret_key: Optional[str] = None):
self.secret_key = secret_key or STRIPE_SECRET_KEY
self.base_url = STRIPE_API_URL
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.secret_key:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Stripe API key not configured"
)
return {
"Authorization": f"Bearer {self.secret_key}",
"Content-Type": "application/x-www-form-urlencoded"
}
async def _request(
self,
method: str,
endpoint: str,
data: Optional[Dict] = None,
params: 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,
params=params,
timeout=30.0
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Stripe API error: {e.response.text}")
error_data = e.response.json().get("error", {})
raise HTTPException(
status_code=e.response.status_code,
detail=error_data.get("message", "Stripe API error")
)
except httpx.HTTPError as e:
logger.error(f"Stripe API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Stripe API request failed: {str(e)}"
)
async def create_payment_intent(
self,
amount: int,
currency: str = "usd",
description: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Dict[str, Any]:
"""Create a payment intent"""
data = {
"amount": amount,
"currency": currency
}
if description:
data["description"] = description
if metadata:
for key, value in metadata.items():
data[f"metadata[{key}]"] = value
return await self._request("POST", "/payment_intents", data=data)
async def get_payment_intent(self, payment_intent_id: str) -> Dict[str, Any]:
"""Get payment intent details"""
return await self._request("GET", f"/payment_intents/{payment_intent_id}")
async def create_customer(
self,
email: str,
name: Optional[str] = None,
description: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Dict[str, Any]:
"""Create a customer"""
data = {"email": email}
if name:
data["name"] = name
if description:
data["description"] = description
if metadata:
for key, value in metadata.items():
data[f"metadata[{key}]"] = value
return await self._request("POST", "/customers", data=data)
async def get_customer(self, customer_id: str) -> Dict[str, Any]:
"""Get customer details"""
return await self._request("GET", f"/customers/{customer_id}")
async def list_customers(self, limit: int = 10) -> Dict[str, Any]:
"""List customers"""
params = {"limit": limit}
return await self._request("GET", "/customers", params=params)
async def create_subscription(
self,
customer_id: str,
price_id: str,
metadata: Optional[Dict] = None
) -> Dict[str, Any]:
"""Create a subscription"""
data = {
"customer": customer_id,
"items[0][price]": price_id
}
if metadata:
for key, value in metadata.items():
data[f"metadata[{key}]"] = value
return await self._request("POST", "/subscriptions", data=data)
async def list_products(self, limit: int = 10) -> Dict[str, Any]:
"""List products"""
params = {"limit": limit}
return await self._request("GET", "/products", params=params)
async def list_prices(self, limit: int = 10) -> Dict[str, Any]:
"""List prices"""
params = {"limit": limit}
return await self._request("GET", "/prices", params=params)
async def get_balance(self) -> Dict[str, Any]:
"""Get account balance"""
return await self._request("GET", "/balance")
# Initialize client
stripe_client = StripeClient()
@router.get("/status")
async def get_stripe_status():
"""Get Stripe API connection status"""
if not STRIPE_SECRET_KEY:
return {
"connected": False,
"message": "Stripe API key not configured. Set STRIPE_SECRET_KEY environment variable."
}
try:
# Try to fetch balance as a health check
await stripe_client.get_balance()
return {
"connected": True,
"message": "Stripe API connected successfully",
"publishable_key_configured": bool(STRIPE_PUBLISHABLE_KEY)
}
except Exception as e:
return {
"connected": False,
"message": f"Stripe API connection failed: {str(e)}"
}
@router.post("/payment-intents")
async def create_payment_intent(payment: PaymentIntent):
"""Create a payment intent"""
try:
intent = await stripe_client.create_payment_intent(
amount=payment.amount,
currency=payment.currency,
description=payment.description,
metadata=payment.metadata
)
return {
"success": True,
"payment_intent": intent,
"client_secret": intent.get("client_secret")
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating payment intent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create payment intent: {str(e)}"
)
@router.get("/payment-intents/{payment_intent_id}")
async def get_payment_intent(payment_intent_id: str):
"""Get payment intent details"""
try:
intent = await stripe_client.get_payment_intent(payment_intent_id)
return intent
except HTTPException:
raise
@router.post("/customers")
async def create_customer(customer: Customer):
"""Create a customer"""
try:
result = await stripe_client.create_customer(
email=customer.email,
name=customer.name,
description=customer.description,
metadata=customer.metadata
)
return {
"success": True,
"customer": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating customer: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create customer: {str(e)}"
)
@router.get("/customers/{customer_id}")
async def get_customer(customer_id: str):
"""Get customer details"""
try:
customer = await stripe_client.get_customer(customer_id)
return customer
except HTTPException:
raise
@router.get("/customers")
async def list_customers(limit: int = 10):
"""List customers"""
try:
result = await stripe_client.list_customers(limit)
return result
except HTTPException:
raise
@router.get("/products")
async def list_products(limit: int = 10):
"""List products"""
try:
result = await stripe_client.list_products(limit)
return result
except HTTPException:
raise
@router.get("/prices")
async def list_prices(limit: int = 10):
"""List prices"""
try:
result = await stripe_client.list_prices(limit)
return result
except HTTPException:
raise
@router.get("/balance")
async def get_balance():
"""Get account balance"""
try:
balance = await stripe_client.get_balance()
return balance
except HTTPException:
raise
@router.get("/health")
async def stripe_health_check():
"""Stripe API health check endpoint"""
return {
"service": "stripe",
"status": "operational" if STRIPE_SECRET_KEY else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,259 @@
"""
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()
}

View File

@@ -0,0 +1,424 @@
"""
Vercel API Integration Router
Provides endpoints for managing Vercel deployments, projects, and domains.
Vercel is a cloud platform for static sites and serverless functions.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/vercel", tags=["vercel"])
# Vercel API configuration
VERCEL_API_URL = "https://api.vercel.com"
VERCEL_TOKEN = os.getenv("VERCEL_TOKEN")
VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID")
class VercelProject(BaseModel):
"""Vercel project model"""
id: str
name: str
framework: Optional[str] = None
created_at: int
updated_at: int
class VercelDeployment(BaseModel):
"""Vercel deployment model"""
uid: str
name: str
url: str
state: str
created_at: int
ready: Optional[int] = None
class VercelDomain(BaseModel):
"""Vercel domain model"""
name: str
verified: bool
created_at: int
class DeploymentTrigger(BaseModel):
"""Trigger deployment request"""
project_id: str
git_branch: Optional[str] = "main"
class VercelClient:
"""Vercel REST API client"""
def __init__(self, token: Optional[str] = None, team_id: Optional[str] = None):
self.token = token or VERCEL_TOKEN
self.team_id = team_id or VERCEL_TEAM_ID
self.base_url = VERCEL_API_URL
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Vercel API token not configured"
)
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def _get_params(self) -> Dict[str, str]:
"""Get query parameters (team ID if configured)"""
params = {}
if self.team_id:
params["teamId"] = self.team_id
return params
async def _request(
self,
method: str,
endpoint: str,
**kwargs
) -> Dict[str, Any]:
"""Make API request"""
headers = self._get_headers()
params = kwargs.pop("params", {})
params.update(self._get_params())
url = f"{self.base_url}{endpoint}"
async with httpx.AsyncClient() as client:
try:
response = await client.request(
method,
url,
headers=headers,
params=params,
timeout=30.0,
**kwargs
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Vercel API error: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Vercel API error: {e.response.text}"
)
except httpx.HTTPError as e:
logger.error(f"Vercel API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Vercel API request failed: {str(e)}"
)
async def get_projects(self) -> List[Dict[str, Any]]:
"""Get all projects"""
data = await self._request("GET", "/v9/projects")
return data.get("projects", [])
async def get_project(self, project_id: str) -> Dict[str, Any]:
"""Get project by ID or name"""
return await self._request("GET", f"/v9/projects/{project_id}")
async def get_deployments(
self,
project_id: Optional[str] = None,
limit: int = 20
) -> List[Dict[str, Any]]:
"""Get deployments"""
params = {"limit": limit}
if project_id:
params["projectId"] = project_id
data = await self._request("GET", "/v6/deployments", params=params)
return data.get("deployments", [])
async def get_deployment(self, deployment_id: str) -> Dict[str, Any]:
"""Get deployment by ID"""
return await self._request("GET", f"/v13/deployments/{deployment_id}")
async def create_deployment(
self,
name: str,
git_source: Optional[Dict[str, str]] = None,
env_vars: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Create a new deployment"""
payload = {"name": name}
if git_source:
payload["gitSource"] = git_source
if env_vars:
payload["env"] = [
{"key": k, "value": v}
for k, v in env_vars.items()
]
return await self._request("POST", "/v13/deployments", json=payload)
async def get_domains(self) -> List[Dict[str, Any]]:
"""Get all domains"""
data = await self._request("GET", "/v5/domains")
return data.get("domains", [])
async def add_domain(self, name: str, project_id: str) -> Dict[str, Any]:
"""Add a domain to a project"""
payload = {"name": name}
return await self._request(
"POST",
f"/v10/projects/{project_id}/domains",
json=payload
)
async def get_env_vars(self, project_id: str) -> List[Dict[str, Any]]:
"""Get environment variables for a project"""
data = await self._request("GET", f"/v9/projects/{project_id}/env")
return data.get("envs", [])
async def create_env_var(
self,
project_id: str,
key: str,
value: str,
target: List[str] = None
) -> Dict[str, Any]:
"""Create environment variable"""
payload = {
"key": key,
"value": value,
"type": "encrypted",
"target": target or ["production", "preview", "development"]
}
return await self._request(
"POST",
f"/v10/projects/{project_id}/env",
json=payload
)
# Initialize client
vercel_client = VercelClient()
@router.get("/status")
async def get_vercel_status():
"""Get Vercel API connection status"""
if not VERCEL_TOKEN:
return {
"connected": False,
"message": "Vercel API token not configured. Set VERCEL_TOKEN environment variable."
}
try:
# Try to fetch user info as a health check
await vercel_client._request("GET", "/v2/user")
return {
"connected": True,
"message": "Vercel API connected successfully",
"team_configured": bool(VERCEL_TEAM_ID)
}
except Exception as e:
return {
"connected": False,
"message": f"Vercel API connection failed: {str(e)}"
}
@router.get("/projects")
async def list_projects():
"""List all Vercel projects"""
try:
projects = await vercel_client.get_projects()
return {"projects": projects, "count": len(projects)}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching projects: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch projects: {str(e)}"
)
@router.get("/projects/{project_id}")
async def get_project(project_id: str):
"""Get project details"""
try:
project = await vercel_client.get_project(project_id)
return project
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching project: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch project: {str(e)}"
)
@router.get("/deployments")
async def list_deployments(project_id: Optional[str] = None, limit: int = 20):
"""List deployments"""
try:
deployments = await vercel_client.get_deployments(project_id, limit)
return {"deployments": deployments, "count": len(deployments)}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching deployments: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch deployments: {str(e)}"
)
@router.get("/deployments/{deployment_id}")
async def get_deployment(deployment_id: str):
"""Get deployment details"""
try:
deployment = await vercel_client.get_deployment(deployment_id)
return deployment
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching deployment: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch deployment: {str(e)}"
)
@router.post("/deployments")
async def create_deployment(
name: str,
git_repo: Optional[str] = None,
git_branch: Optional[str] = "main"
):
"""Create a new deployment"""
try:
git_source = None
if git_repo:
git_source = {
"type": "github",
"repo": git_repo,
"ref": git_branch
}
deployment = await vercel_client.create_deployment(name, git_source)
return {
"success": True,
"deployment": deployment,
"message": "Deployment created successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating deployment: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create deployment: {str(e)}"
)
@router.get("/domains")
async def list_domains():
"""List all domains"""
try:
domains = await vercel_client.get_domains()
return {"domains": domains, "count": len(domains)}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching domains: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch domains: {str(e)}"
)
@router.post("/projects/{project_id}/domains")
async def add_domain(project_id: str, domain: str):
"""Add a domain to a project"""
try:
result = await vercel_client.add_domain(domain, project_id)
return {
"success": True,
"domain": result,
"message": "Domain added successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error adding domain: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add domain: {str(e)}"
)
@router.get("/projects/{project_id}/env")
async def get_env_vars(project_id: str):
"""Get environment variables for a project"""
try:
env_vars = await vercel_client.get_env_vars(project_id)
return {"variables": env_vars, "count": len(env_vars)}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching env vars: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch env vars: {str(e)}"
)
@router.post("/projects/{project_id}/env")
async def create_env_var(
project_id: str,
key: str,
value: str,
target: Optional[List[str]] = None
):
"""Create an environment variable"""
try:
result = await vercel_client.create_env_var(
project_id,
key,
value,
target
)
return {
"success": True,
"variable": result,
"message": "Environment variable created successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating env var: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create env var: {str(e)}"
)
@router.get("/health")
async def vercel_health_check():
"""Vercel API health check endpoint"""
return {
"service": "vercel",
"status": "operational" if VERCEL_TOKEN else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}