connect / routes /support.py
habulaj's picture
Update routes/support.py
b48ebe4 verified
raw
history blame
17.1 kB
import os
import logging
import aiohttp
import base64
from fastapi import APIRouter, HTTPException, Header, Body
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
router = APIRouter()
# 🔧 Supabase Config
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
SUPABASE_KEY = os.getenv("SUPA_KEY")
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
if not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
raise ValueError("❌ SUPA_KEY or SUPA_SERVICE_KEY not set in environment!")
SUPABASE_HEADERS = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json"
}
SUPABASE_ROLE_HEADERS = {
"apikey": SUPABASE_ROLE_KEY,
"Authorization": f"Bearer {SUPABASE_ROLE_KEY}",
"Content-Type": "application/json",
"Prefer": "return=representation"
}
# 🛡️ Verificação de token de usuário (sem admin check)
async def verify_user_token(user_token: str) -> str:
headers = {
"Authorization": f"Bearer {user_token}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
async with aiohttp.ClientSession() as session:
async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
if response.status != 200:
raise HTTPException(status_code=401, detail="Token inválido ou expirado")
user_data = await response.json()
user_id = user_data.get("id")
if not user_id:
raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
return user_id
# 📨 Modelo da requisição de ticket
class CreateTicketRequest(BaseModel):
message: str
@router.get("/ticket/user")
async def get_user_tickets(user_id: str):
async with aiohttp.ClientSession() as session:
# 1. Buscar os últimos 50 tickets do usuário
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?user_id=eq.{user_id}&order=created_at.desc&limit=50",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
error_detail = await ticket_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao buscar tickets: {error_detail}")
tickets = await ticket_resp.json()
# 2. Para cada ticket, buscar a última mensagem
ticket_results = []
for ticket in tickets:
ticket_id = ticket["id"]
async with session.get(
f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.desc&limit=1",
headers=SUPABASE_ROLE_HEADERS
) as msg_resp:
if msg_resp.status != 200:
continue # apenas ignora caso erro
messages = await msg_resp.json()
last_message = messages[0] if messages else None
ticket_results.append({
"ticket": ticket,
"last_message": last_message
})
return ticket_results
@router.get("/ticket/detail")
async def get_ticket_details(ticket_id: int):
async with aiohttp.ClientSession() as session:
# 1. Buscar dados do ticket
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket não encontrado")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket inexistente")
ticket = ticket_data[0]
# 2. Buscar as 50 últimas mensagens
async with session.get(
f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.desc&limit=50",
headers=SUPABASE_ROLE_HEADERS
) as msg_resp:
if msg_resp.status != 200:
raise HTTPException(status_code=500, detail="Erro ao buscar mensagens")
messages_raw = await msg_resp.json()
# 3. Buscar info dos usuários das mensagens
user_cache = {}
for msg in messages_raw:
user_id = msg["user"]
if user_id not in user_cache:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
if user_resp.status == 200:
user_data = await user_resp.json()
if user_data:
u = user_data[0]
user_cache[user_id] = {
"id": u["id"],
"name": u.get("name", "Desconhecido"),
"avatar": u.get("avatar", None),
"type": "support" if u.get("role") == "admin" else "customer"
}
else:
user_cache[user_id] = {
"id": user_id,
"name": "Desconhecido",
"avatar": None,
"type": "unknown"
}
# 4. Substituir o campo user nas mensagens
messages = []
for msg in messages_raw:
uid = msg["user"]
user_info = user_cache.get(uid, {
"id": uid,
"name": "Desconhecido",
"avatar": None,
})
# Definir o tipo com base no ticket
if uid == ticket["user_id"]:
user_info["type"] = "customer"
else:
user_info["type"] = "support"
messages.append({
**msg,
"user": user_info
})
return {
"ticket": ticket,
"messages": messages
}
class RespondTicketRequest(BaseModel):
ticket_id: int
content: str
@router.post("/ticket/respond")
async def respond_ticket(
ticket_id: int,
content: str,
user_token: str = Header(None, alias="User-key")
):
# 1. Verificar se o usuário está autenticado com o token
user_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
# 2. Verificar se o ticket existe
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket não encontrado")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket inexistente")
ticket = ticket_data[0]
# 3. Criar a mensagem de resposta ao ticket
message_payload = {
"user": user_id,
"content": content,
"created_at": created_at,
"ticket_id": ticket_id
}
# 4. Salvar a mensagem no banco
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as message_resp:
if message_resp.status != 201:
error_detail = await message_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao salvar a mensagem: {error_detail}")
message_data = await message_resp.json()
# 5. Retornar confirmação da resposta
return {
"status": "response sent successfully",
"ticket_id": ticket_id,
"message_id": message_data[0]["id"],
"message_content": content
}
@router.post("/ticket/create")
async def create_ticket(
body: CreateTicketRequest,
user_token: str = Header(None, alias="User-key")
):
user_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
ticket_payload = {
"user_id": user_id,
"support_id": None,
"created_at": created_at
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/Tickets",
headers=SUPABASE_ROLE_HEADERS,
json=ticket_payload
) as ticket_resp:
if ticket_resp.status != 201:
error_detail = await ticket_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao criar ticket: {error_detail}")
ticket_data = await ticket_resp.json()
ticket_id = ticket_data[0]["id"]
message_payload = {
"user": user_id,
"content": body.message,
"created_at": created_at,
"ticket_id": ticket_id
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as message_resp:
if message_resp.status != 201:
error_detail = await message_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao criar mensagem: {error_detail}")
return {"ticket_id": ticket_id}
# 📧 Envio de e-mails com Gmail API
GMAIL_CLIENT_ID = "784687789817-3genmmvps11ip3a6fkbkkd8dm3bstgdc.apps.googleusercontent.com"
GMAIL_CLIENT_SECRET = "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"
GMAIL_REFRESH_TOKEN = "1//04ZOO_chVwlYiCgYIARAAGAQSNwF-L9IrhQO1ij79thk-DTjiMudl_XQshuU5CDTDYtt8rrOTMbz_rL8ECGjNfEN9da6W-mnjhZA"
class TicketResponseRequest(BaseModel):
ticket_id: int
content: str
async def get_gmail_access_token() -> str:
url = "https://oauth2.googleapis.com/token"
data = {
"client_id": GMAIL_CLIENT_ID,
"client_secret": GMAIL_CLIENT_SECRET,
"refresh_token": GMAIL_REFRESH_TOKEN,
"grant_type": "refresh_token"
}
async with aiohttp.ClientSession() as session:
async with session.post(url, data=data) as response:
if response.status != 200:
raise HTTPException(status_code=500, detail="Erro ao obter access_token do Gmail")
token_data = await response.json()
return token_data["access_token"]
def encode_message(raw_message: str) -> str:
return base64.urlsafe_b64encode(raw_message.encode("utf-8")).decode("utf-8").replace("=", "")
@router.post("/ticket/support/respond")
async def respond_ticket(
payload: TicketResponseRequest,
user_token: str = Header(None, alias="User-key")
):
# 1. Verify support agent token
support_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
# Get support agent name
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{support_id}",
headers=SUPABASE_ROLE_HEADERS
) as support_user_resp:
if support_user_resp.status != 200:
raise HTTPException(status_code=404, detail="Support agent not found")
support_user_data = await support_user_resp.json()
if not support_user_data:
raise HTTPException(status_code=404, detail="Support agent does not exist")
support_name = support_user_data[0].get("name", "Support Team").strip()
# 2. Retrieve ticket
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{payload.ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket not found")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket does not exist")
ticket = ticket_data[0]
user_id = ticket["user_id"]
# 3. Get user email and name
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
if user_resp.status != 200:
raise HTTPException(status_code=404, detail="User not found")
user_data = await user_resp.json()
if not user_data:
raise HTTPException(status_code=404, detail="User does not exist")
user_email = user_data[0]["email"]
user_name = user_data[0].get("name", "user").strip()
first_name = user_name.split(" ")[0] if user_name else "user"
# 4. Send email with clean, elegant HTML template with brand purple gradient
access_token = await get_gmail_access_token()
subject = f"Customer Support - Case {payload.ticket_id}"
email_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body style="margin:0;padding:0;background-color:#f9f9f9;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f9f9f9">
<tr>
<td align="center" style="padding:40px 10px;">
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;box-shadow:0 2px 4px rgba(0,0,0,0.1);">
<tr>
<td style="height:5px;background:linear-gradient(to right, #6E2FC6, #9662D9);"></td>
</tr>
<tr>
<td style="padding:30px 40px;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:14px;line-height:1.6;color:#333333;">
<p style="margin:0 0 20px;font-size:15px;color:#333333;">Hello {first_name},</p>
<div style="margin:0 0 20px;color:#333333;">
{payload.content}
</div>
<p style="margin:20px 0 0;font-size:15px;color:#333333;">Best regards,</p>
<p style="margin:2px 0 0;font-size:15px;color:#333333;">{support_name}</p>
<p style="margin:2px 0 0;font-size:14px;color:#666666;">Customer Support</p>
</td>
</tr>
<tr>
<td style="height:1px;background:#f0f0f0;"></td>
</tr>
<tr>
<td style="padding:20px;text-align:center;font-size:12px;color:#999999;">
ClosetCoach © 2025
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
"""
raw_message = f"""To: {user_email}
From: "ClosetCoach" <streamify@ameddes.com>
Subject: {subject}
Content-Type: text/html; charset="UTF-8"
{email_html}
"""
encoded_message = encode_message(raw_message)
async with aiohttp.ClientSession() as session:
async with session.post(
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
json={"raw": encoded_message}
) as gmail_resp:
if gmail_resp.status != 200:
error_detail = await gmail_resp.text()
raise HTTPException(status_code=500, detail=f"Error sending email: {error_detail}")
gmail_data = await gmail_resp.json()
# 5. Save response in messages_tickets
message_payload = {
"user": support_id,
"content": payload.content,
"created_at": created_at,
"ticket_id": payload.ticket_id
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as msg_resp:
if msg_resp.status != 201:
error_detail = await msg_resp.text()
raise HTTPException(status_code=500, detail=f"Error recording response: {error_detail}")
return {
"status": "response sent successfully",
"ticket_id": payload.ticket_id,
"email_to": user_email,
"message_id": gmail_data.get("id")
}