privacy policy and business integration
This commit is contained in:
parent
141ca9fda3
commit
cd3751219d
16 changed files with 1592 additions and 30 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue