privacy policy and business integration

This commit is contained in:
debu-2 2026-10-02 14:09:03 +05:30
parent 141ca9fda3
commit cd3751219d
16 changed files with 1592 additions and 30 deletions

View file

@ -1,7 +1,6 @@
from datetime import datetime, timezone
from uuid import UUID
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session, joinedload
@ -22,9 +21,24 @@ from .models import (
TemplateConfig,
UrmiOtpProject,
UserAccount,
WhatsAppMessage,
)
from .phone_number_validators import normalize_phone_number, validate_per_min_price, validate_price
from .routes_urmiotp import serialize_project
from .whatsapp_config import (
resolved_whatsapp_app_secret,
resolved_whatsapp_business_account_id,
resolved_whatsapp_phone_number_id,
whatsapp_ready_to_send,
)
from .whatsapp_graph_client import WhatsAppGraphClient, WhatsAppGraphError
from .whatsapp_messages import (
extract_wa_message_id,
normalize_wa_number,
record_outbound_message,
serialize_message,
)
from .platform_settings import set_setting
router = APIRouter(prefix="/api/v1/admin", tags=["admin"])
@ -516,3 +530,244 @@ def update_phone_number(
db.commit()
db.refresh(number)
return serialize_admin_phone_number(number)
class AdminWhatsAppSendRequest(BaseModel):
to: str = Field(min_length=8, max_length=20)
message_type: Literal[
"text",
"template",
"image",
"document",
"audio",
"video",
"interactive_buttons",
"interactive_list",
"reaction",
] = "text"
text: str | None = Field(default=None, max_length=4096)
reply_to: str | None = None
template_name: str | None = None
template_language: str = "en"
template_components: list[dict] | None = None
media_link: str | None = None
media_caption: str | None = None
media_filename: str | None = None
interactive_body: str | None = None
interactive_header: str | None = None
interactive_footer: str | None = None
interactive_buttons: list[dict[str, str]] | None = None
interactive_list_button: str | None = None
interactive_list_sections: list[dict] | None = None
reaction_message_id: str | None = None
reaction_emoji: str | None = None
class AdminWhatsAppConfigUpdate(BaseModel):
phone_number_id: str | None = None
business_account_id: str | None = None
app_secret: str | None = None
def _whatsapp_client(db: Session) -> WhatsAppGraphClient:
return WhatsAppGraphClient(
phone_number_id=resolved_whatsapp_phone_number_id(db),
business_account_id=resolved_whatsapp_business_account_id(db),
)
@router.get("/whatsapp/config")
def admin_whatsapp_config(db: Session = Depends(get_db), _: str = Depends(current_admin)) -> dict:
return {
"business_phone": settings.whatsapp_business_phone,
"phone_number_id": resolved_whatsapp_phone_number_id(db) or None,
"business_account_id": resolved_whatsapp_business_account_id(db) or None,
"meta_app_id": settings.meta_app_id or None,
"access_token_configured": bool(settings.whatsapp_access_token),
"app_secret_configured": bool(resolved_whatsapp_app_secret(db)),
"webhook_url": settings.whatsapp_webhook_url,
"verify_token_configured": bool(settings.whatsapp_verify_token),
"ready_to_send": whatsapp_ready_to_send(db),
"supported_operations": [
"text",
"template",
"image",
"document",
"audio",
"video",
"interactive_buttons",
"interactive_list",
"reaction",
"mark_read",
"media_download",
"list_templates",
],
}
@router.patch("/whatsapp/config")
def admin_update_whatsapp_config(
payload: AdminWhatsAppConfigUpdate,
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> dict:
if payload.phone_number_id is not None:
set_setting(db, "whatsapp_phone_number_id", payload.phone_number_id.strip())
if payload.business_account_id is not None:
set_setting(db, "whatsapp_business_account_id", payload.business_account_id.strip())
if payload.app_secret is not None:
set_setting(db, "whatsapp_app_secret", payload.app_secret.strip())
return admin_whatsapp_config(db=db, _=_)
@router.get("/whatsapp/messages")
def admin_list_whatsapp_messages(
limit: int = 50,
peer: str | None = None,
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> list[dict]:
query = select(WhatsAppMessage).order_by(WhatsAppMessage.created_at.desc()).limit(min(limit, 500))
if peer:
try:
normalized_peer = normalize_wa_number(peer)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
query = query.where(
(WhatsAppMessage.from_number == normalized_peer) | (WhatsAppMessage.to_number == normalized_peer)
)
messages = db.scalars(query).all()
return [serialize_message(message) for message in messages]
@router.get("/whatsapp/templates")
async def admin_list_whatsapp_templates(
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> dict:
client = _whatsapp_client(db)
try:
return await client.list_templates()
except WhatsAppGraphError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
@router.get("/whatsapp/media/{media_id}")
async def admin_download_whatsapp_media(
media_id: str,
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> Response:
client = _whatsapp_client(db)
try:
content, mime_type = await client.download_media(media_id)
except WhatsAppGraphError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
return Response(content=content, media_type=mime_type)
@router.post("/whatsapp/messages/{wa_message_id}/read")
async def admin_mark_whatsapp_read(
wa_message_id: str,
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> dict:
client = _whatsapp_client(db)
try:
result = await client.mark_read(wa_message_id)
except WhatsAppGraphError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
return result
@router.post("/whatsapp/messages/send")
async def admin_send_whatsapp_message(
payload: AdminWhatsAppSendRequest,
db: Session = Depends(get_db),
_: str = Depends(current_admin),
) -> dict:
try:
recipient = normalize_wa_number(payload.to)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
client = _whatsapp_client(db)
message_type = payload.message_type
try:
if message_type == "text":
if not payload.text:
raise HTTPException(status_code=422, detail="text is required")
result = await client.send_text(recipient, payload.text.strip(), reply_to=payload.reply_to)
body = payload.text.strip()
elif message_type == "template":
if not payload.template_name:
raise HTTPException(status_code=422, detail="template_name is required")
result = await client.send_template(
recipient,
payload.template_name,
language_code=payload.template_language,
components=payload.template_components,
)
body = f"Template: {payload.template_name}"
elif message_type in {"image", "document", "audio", "video"}:
if not payload.media_link:
raise HTTPException(status_code=422, detail="media_link is required")
result = await client.send_media(
recipient,
message_type,
link=payload.media_link,
caption=payload.media_caption,
filename=payload.media_filename,
reply_to=payload.reply_to,
)
body = payload.media_caption or f"[{message_type}]"
elif message_type == "interactive_buttons":
if not payload.interactive_body or not payload.interactive_buttons:
raise HTTPException(status_code=422, detail="interactive_body and interactive_buttons are required")
result = await client.send_interactive_buttons(
recipient,
payload.interactive_body,
payload.interactive_buttons,
header=payload.interactive_header,
footer=payload.interactive_footer,
reply_to=payload.reply_to,
)
body = payload.interactive_body
message_type = "interactive"
elif message_type == "interactive_list":
if not payload.interactive_body or not payload.interactive_list_button or not payload.interactive_list_sections:
raise HTTPException(
status_code=422,
detail="interactive_body, interactive_list_button, and interactive_list_sections are required",
)
result = await client.send_interactive_list(
recipient,
payload.interactive_body,
payload.interactive_list_button,
payload.interactive_list_sections,
header=payload.interactive_header,
footer=payload.interactive_footer,
reply_to=payload.reply_to,
)
body = payload.interactive_body
message_type = "interactive"
elif message_type == "reaction":
if not payload.reaction_message_id or not payload.reaction_emoji:
raise HTTPException(status_code=422, detail="reaction_message_id and reaction_emoji are required")
result = await client.send_reaction(recipient, payload.reaction_message_id, payload.reaction_emoji)
body = payload.reaction_emoji
else:
raise HTTPException(status_code=422, detail="Unsupported message type")
except WhatsAppGraphError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
message = record_outbound_message(
db,
to_number=recipient,
body=body,
message_type=message_type,
wa_message_id=extract_wa_message_id(result),
status="sent",
payload=result if isinstance(result, dict) else None,
)
return serialize_message(message)